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"
"path/filepath"
"runtime"
"runtime/debug"
"time"
"github.com/mitchellh/go-homedir"
@ -26,12 +27,17 @@ const (
// DefaultVersion is the initial value for string members whose implicit type
// is a semver.
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.
type Client struct {
repositories *Repositories // Repositories management
templates *Templates // Templates management
repositoriesPath string // path to repositories
repositoriesURI string // repo URI (overrides repositories path)
verbose bool // print verbose logs
builder Builder // Builds a runnable image source
pusher Pusher // Pushes Funcation image to a remote
@ -44,6 +50,8 @@ type Client struct {
registry string // default registry for OCI image tags
progressListener ProgressListener // progress listener
emitter Emitter // Emits CloudEvents to functions
repositories *Repositories // Repositories management
templates *Templates // Templates management
}
// ErrNotBuilt indicates the Function has not yet been built.
@ -161,14 +169,8 @@ type Emitter interface {
// New client for Function management.
func New(options ...Option) *Client {
// Assert the global config directory exists (including child directories
// such as repositories)
assertConfigDir()
// Instantiate client with static defaults.
c := &Client{
repositories: &Repositories{},
templates: &Templates{},
builder: &noopBuilder{output: os.Stdout},
pusher: &noopPusher{output: os.Stdout},
deployer: &noopDeployer{output: os.Stdout},
@ -178,54 +180,61 @@ func New(options ...Option) *Client {
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &NoopProgressListener{},
emitter: &noopEmitter{},
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
}
c.repositories = newRepositories(c)
c.templates = newTemplates(c)
for _, o := range options {
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
}
// assertConfigDir makes a best-effort attempt to create the config directory
// (including required subdirectories).
func assertConfigDir() {
// NOTE: regarding running as a user with no home directory:
// The default is .config/func in current working directory when there is no
// available HOME in which to find .`~/.config/func`.
// Since it is expected that the code elsewhere never assume the config
// directory exists (doing so is a racing condition), it is valid to simply
// 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")
}
// The default config path is evaluated in the following order, from lowest
// to highest precedence.
// 1. The static default is DefaultConfigPath (./.config/func)
// 2. ~/.config/func if it exists (can be expanded: user has a home dir)
// 3. The value of $XDG_CONFIG_PATH/func if the environment variable exists.
// The path will be created if it does not already exist.
func ConfigPath() (path string) {
path = DefaultConfigPath
// ~/.config/func is the default if ~ can be expanded
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
// the current working directory when neither aforementioned exist.
return ".config/func"
// 'XDG_CONFIG_HOME/func' takes precidence if defined
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
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
@ -308,21 +317,21 @@ func WithDNSProvider(provider DNSProvider) Option {
}
}
// WithRepositories sets the location to use for extensible template repositories.
// Extensible template repositories are additional templates that exist on disk and are
// not built into the binary.
// WithRepositories sets the location to use for extensible template
// repositories. Extensible template repositories are additional templates
// that exist on disk and are not built into the binary.
func WithRepositories(path string) Option {
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.
// This setting's existence precldes the use of either the inbuilt templates or any
// repositories from the extensible repositories path.
// WithRepository sets a specific URL to a Git repository from which to pull
// templates. This setting's existence precldes the use of either the inbuilt
// templates or any repositories from the extensible repositories path.
func WithRepository(uri string) Option {
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) Stopping() {}
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(
fn.WithRegistry(DefaultRegistry),
fn.WithRepository("https://github.com/boson-project/test-templates"),
fn.WithRepositories("testdata/repositories"),
)
err := client.Create(fn.Function{
Root: ".",

View File

@ -246,7 +246,7 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
// it is still available as an environment variable.
repositories = os.Getenv("FUNC_REPOSITORIES")
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.

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")
sys := &containersTypes.SystemContext{
AuthFilePath: authFilePath,

View File

@ -31,9 +31,16 @@ type Repositories struct {
path string
// 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
// 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
}
@ -42,32 +49,17 @@ type Repositories struct {
// full client API during implementations.
func newRepositories(client *Client) *Repositories {
return &Repositories{
path: DefaultRepositoriesPath,
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.
func (r *Repositories) Path() string {
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
// defined.
func (r *Repositories) List() ([]string, error) {