feat: readonly global config (#1260)

* improved deploy test output

* remove unused config struct

* feat: read-only global config

Co-authored-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Luke Kingland 2022-09-24 02:49:13 +09:00 committed by GitHub
parent 4be773d9f6
commit 4c8f730099
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 333 additions and 57 deletions

View File

@ -14,7 +14,7 @@ import (
"runtime/debug"
"time"
"github.com/mitchellh/go-homedir"
"knative.dev/kn-plugin-func/config"
)
const (
@ -27,11 +27,6 @@ const (
// includes an HTTP Handler ("http") and Cloud Events handler ("events")
DefaultTemplate = "http"
// 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"
// RunDataDir holds transient runtime metadata
// By default it is excluded from source control.
RunDataDir = ".func"
@ -207,7 +202,7 @@ func New(options ...Option) *Client {
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &NoopProgressListener{},
pipelinesProvider: &noopPipelinesProvider{},
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
repositoriesPath: filepath.Join(config.Path(), "repositories"),
transport: http.DefaultTransport,
}
for _, o := range options {
@ -220,35 +215,12 @@ func New(options ...Option) *Client {
c.instances = newInstances(c)
// Trigger the creation of the config and repository paths
_ = ConfigPath() // Config is package-global scoped
_ = config.Path()
_ = c.RepositoriesPath() // Repositories is Client-specific
return c
}
// 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 {
path = filepath.Join(home, ".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 WithRepositoriesPath option when creating the client..

View File

@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"knative.dev/kn-plugin-func/buildpacks"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/s2i"
fn "knative.dev/kn-plugin-func"
@ -50,9 +51,15 @@ and the image name is stored in the configuration file.
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm", "push", "builder-image", "platform"),
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
cmd.Flags().StringP("builder", "b", builders.Default, fmt.Sprintf("build strategy to use when creating the underlying image. Currently supported build strategies are %s.", KnownBuilders()))
cmd.Flags().StringP("builder-image", "", "", "builder image, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml (as 'builder' field) for subsequent builds. ($FUNC_BUILDER_IMAGE)")
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined (Env: $FUNC_REGISTRY)")
cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built")

View File

@ -13,6 +13,7 @@ import (
"github.com/spf13/cobra"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/utils"
)
@ -74,11 +75,17 @@ EXAMPLES
PreRunE: bindEnv("language", "template", "repository", "confirm"),
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
// Flags
cmd.Flags().StringP("language", "l", "", "Language Runtime (see help text for list) (Env: $FUNC_LANGUAGE)")
cmd.Flags().StringP("language", "l", cfg.Language, "Language Runtime (see help text for list) (Env: $FUNC_LANGUAGE)")
cmd.Flags().StringP("template", "t", fn.DefaultTemplate, "Function template. (see help text for list) (Env: $FUNC_TEMPLATE)")
cmd.Flags().StringP("repository", "r", "", "URI to a Git repository containing the specified template (Env: $FUNC_REPOSITORY)")
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
// Help Action
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { runCreateHelp(cmd, args, newClient) })

View File

@ -107,3 +107,27 @@ func TestCreateConfig_RepositoriesPath(t *testing.T) {
t.Fatalf("expected repositories default path to be '%v', got '%v'", expected, cfg.RepositoriesPath)
}
}
// TestCreate_ConfigOptional ensures that the system can be used without
// any additional configuration being required.
func TestCreate_ConfigOptional(t *testing.T) {
// Empty Home
// the func directory, and other static assets will be created here
// if they do not exist.
home, rm := Mktemp(t)
defer rm()
t.Setenv("XDG_CONFIG_HOME", home)
// Immediately using "create" in a new empty directory should not fail;
// even when this home directory is devoid of config files.
_, rm2 := Mktemp(t)
defer rm2()
cmd := NewCreateCmd(NewClient)
cmd.SetArgs([]string{"--language=go"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
// Not failing is success. Config files or settings beyond what are
// automatically written to to the given config home are currently optional.
}

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
)
func NewDeleteCmd(newClient ClientFactory) *cobra.Command {
@ -36,7 +37,14 @@ No local files are deleted.
SilenceUsage: true, // no usage dump on error
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
// Flag
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
cmd.Flags().StringP("all", "a", "true", "Delete all resources created for a function, eg. Pipelines, Secrets, etc. (Env: $FUNC_ALL) (allowed values: \"true\", \"false\")")
setPathFlag(cmd)

View File

@ -20,6 +20,7 @@ import (
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/builders"
"knative.dev/kn-plugin-func/buildpacks"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/docker"
"knative.dev/kn-plugin-func/docker/creds"
"knative.dev/kn-plugin-func/k8s"
@ -104,7 +105,14 @@ EXAMPLES
PreRunE: bindEnv("confirm", "env", "git-url", "git-branch", "git-dir", "remote", "build", "builder", "builder-image", "image", "registry", "push", "platform", "path", "namespace"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
// Flags
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
cmd.Flags().StringArrayP("env", "e", []string{}, "Environment variable to set in the form NAME=VALUE. "+
"You may provide this flag multiple times for setting multiple environment variables. "+
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")

View File

@ -285,7 +285,7 @@ func testBuilderPersists(cmdFn commandConstructor, t *testing.T) {
t.Fatal(err)
}
if f.Build.Builder != builders.S2I {
t.Fatal("value of builder flag not persisted when provided")
t.Fatalf("value of builder flag not persisted when provided. Expected '%v' got '%v'", builders.S2I, f.Build.Builder)
}
// Build the function again without specifying a Builder
@ -316,19 +316,19 @@ func testBuilderPersists(cmdFn commandConstructor, t *testing.T) {
t.Fatal(err)
}
if f.Build.Builder != builders.Pack {
t.Fatal("value of builder flag not persisted on subsequent build")
t.Fatalf("value of builder flag not persisted on subsequent build. Expected '%v' got '%v'", builders.Pack, f.Build.Builder)
}
// Build the function, specifying a platform with "pack" Builder
cmd.SetArgs([]string{"--platform", "linux"})
if err := cmd.Execute(); err == nil {
t.Fatal("Expected error")
t.Fatal("Expected error using --platform without s2i builder was not received")
}
// Set an invalid builder
cmd.SetArgs([]string{"--builder", "invalid"})
if err := cmd.Execute(); err == nil {
t.Fatal("Expected error")
t.Fatal("Expected error using an invalid --builder not received")
}
}

View File

@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/utils"
)
@ -105,6 +106,12 @@ EXAMPLES
PreRunE: bindEnv("path", "format", "target", "id", "source", "type", "data", "content-type", "file", "insecure", "confirm"),
}
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
// Flags
setPathFlag(cmd)
cmd.Flags().StringP("format", "f", "", "Format of message to send, 'http' or 'cloudevent'. Default is to choose automatically. (Env: $FUNC_FORMAT)")
@ -116,7 +123,7 @@ EXAMPLES
cmd.Flags().StringP("data", "", fn.DefaultInvokeData, "Data to send in the request. (Env: $FUNC_DATA)")
cmd.Flags().StringP("file", "", "", "Path to a file to use as data. Overrides --data flag and should be sent with a correct --content-type. (Env: $FUNC_FILE)")
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure server connections when using SSL. (Env: $FUNC_INSECURE)")
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively. (Env: $FUNC_CONFIRM)")
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively. (Env: $FUNC_CONFIRM)")
cmd.SetHelpFunc(defaultTemplatedHelp)

View File

@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
)
// command constructors
@ -140,7 +141,14 @@ EXAMPLES
PreRunE: bindEnv("confirm"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
// Config
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
// Flags
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cmd.SetHelpFunc(defaultTemplatedHelp)
@ -177,7 +185,12 @@ func NewRepositoryAddCmd(newClient ClientFactory) *cobra.Command {
PreRunE: bindEnv("confirm"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
return runRepositoryAdd(cmd, args, newClient)
@ -193,7 +206,12 @@ func NewRepositoryRenameCmd(newClient ClientFactory) *cobra.Command {
PreRunE: bindEnv("confirm"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
return runRepositoryRename(cmd, args, newClient)
@ -211,7 +229,12 @@ func NewRepositoryRemoveCmd(newClient ClientFactory) *cobra.Command {
PreRunE: bindEnv("confirm"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cfg, err := config.NewDefault()
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.ConfigPath(), err)
}
cmd.Flags().BoolP("confirm", "c", cfg.Confirm, "Prompt to confirm all options interactively (Env: $FUNC_CONFIRM)")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
return runRepositoryRemove(cmd, args, newClient)

View File

@ -1,5 +0,0 @@
package function
// Config is local and global configuration which is not "part of the function"
// and is thus not likely to be tracked in source control.
type Config struct{}

121
config/config.go Normal file
View File

@ -0,0 +1,121 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v2"
)
const (
// Filename into which Config is serialized
Filename = "config.yaml"
// DefaultConfigPath is used in the unlikely event that
// the user has no home directory (no ~), there is no
// XDG_CONFIG_HOME set
DefaultConfigPath = ".config/func"
// DefaultLanguage is intentionaly undefined.
DefaultLanguage = ""
)
type Config struct {
// Language Runtime
Language string `yaml:"language"`
// Confirm Prompts
Confirm bool `yaml:"confirm"`
}
// New Config struct with all members set to static defaults. See NewDefaults
// for one which further takes into account the optional config file.
func New() Config {
return Config{
Language: DefaultLanguage,
// ...
}
}
// Creates a new config populated by global defaults as defined by the
// config file located in .Path() (the global func settings path, which is
// usually ~/.config/func)
func NewDefault() (cfg Config, err error) {
cfg = New() // cfg now populated by static defaults
p := ConfigPath() // applies ~/.config/func/config.yaml if it exists
if _, err = os.Stat(p); err != nil {
if os.IsNotExist(err) {
err = nil // config file is not required
}
return
}
bb, err := os.ReadFile(p)
if err != nil {
return
}
err = yaml.Unmarshal(bb, &cfg) // cfg now has applied config.yaml
return
}
// Load the config exactly as it exists at path (no static defaults)
func Load(path string) (c Config, err error) {
if _, err = os.Stat(path); err != nil {
return
}
bb, err := os.ReadFile(path)
if err != nil {
return
}
err = yaml.Unmarshal(bb, &c)
return
}
// Save the config to the given path
func (c Config) Save(path string) (err error) {
var bb []byte
if bb, err = yaml.Marshal(&c); err != nil {
return
}
return ioutil.WriteFile(path, bb, os.ModePerm)
}
// Path is derived 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 is created if it does not already exist.
func Path() (path string) {
path = DefaultConfigPath
// ~/.config/func is the default if ~ can be expanded
if home, err := homedir.Expand("~"); err == nil {
path = filepath.Join(home, ".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)
return
}
// ConfigPath returns the full path at which to look for a config file.
func ConfigPath() string {
// TODO: It might be nice to include consideration of a FUNC_CONFIG_FILE
// which would allow explicitly setting a config file.
// usually ~/.config/func/config.yaml
return filepath.Join(Path(), Filename)
}
func mkdir(path string) {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
fmt.Fprintf(os.Stderr, "Error creating '%v': %v", path, err)
}
}

94
config/config_test.go Normal file
View File

@ -0,0 +1,94 @@
package config_test
import (
"path/filepath"
"testing"
"knative.dev/kn-plugin-func/config"
. "knative.dev/kn-plugin-func/testing"
)
// TestNewDefaults ensures that the default Config
// constructor yelds a struct prepopulated with static
// defaults.
func TestNewDefaults(t *testing.T) {
cfg := config.New()
if cfg.Language != config.DefaultLanguage {
t.Fatalf("expected config's language = '%v', got '%v'", config.DefaultLanguage, cfg.Language)
}
}
// TestLoad ensures that loading a config reads values
// in from a config file at path.
func TestLoad(t *testing.T) {
cfg, err := config.Load("testdata/func/config.yaml")
if err != nil {
t.Fatal(err)
}
if cfg.Language != "custom" {
t.Fatalf("loaded config did not contain values from config file. Expected \"custom\" got \"%v\"", cfg.Language)
}
}
// TestSave ensures that saving an update config persists.
func TestSave(t *testing.T) {
// mktmp
root, rm := Mktemp(t)
defer rm()
// touch config.yaml
filename := filepath.Join(root, "config.yaml")
// update
cfg := config.New()
cfg.Language = "testSave"
// save
if err := cfg.Save(filename); err != nil {
t.Fatal(err)
}
// reload
cfg, err := config.Load(filename)
if err != nil {
t.Fatal(err)
}
// assert persisted
if cfg.Language != "testSave" {
t.Fatalf("config did not persist. expected 'testSave', got '%v'", cfg.Language)
}
}
// TestPath ensures that the Path accessor returns
// XDG_CONFIG_HOME/.config/func
func TestPath(t *testing.T) {
home := t.TempDir() // root of all configs
path := filepath.Join(home, "func") // our config
t.Setenv("XDG_CONFIG_HOME", home)
if config.Path() != path {
t.Fatalf("expected config path '%v', got '%v'", path, config.Path())
}
}
// TestNewDefault ensures that the default returned from NewDefault includes
// both the static defaults (see TestNewDefaults), as well as those from the
// currently effective global config path (~/config/func).
func TestNewDefault(t *testing.T) {
// Custom config home results in a config file default path of
// ./testdata/func/config.yaml
home := filepath.Join(Cwd(), "testdata")
t.Setenv("XDG_CONFIG_HOME", home)
cfg, err := config.NewDefault() // Should load values from above config
if err != nil {
t.Fatal(err)
}
if cfg.Language != "custom" {
t.Fatalf("config file not loaded")
}
}

1
config/testdata/func/config.yaml vendored Normal file
View File

@ -0,0 +1 @@
language: custom

View File

@ -14,10 +14,10 @@ import (
"runtime"
"strings"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/docker"
"github.com/containers/image/v5/pkg/docker/config"
dockerConfig "github.com/containers/image/v5/pkg/docker/config"
containersTypes "github.com/containers/image/v5/types"
"github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
@ -167,7 +167,7 @@ func NewCredentialsProvider(opts ...Opt) docker.CredentialsProvider {
}
}
c.authFilePath = filepath.Join(fn.ConfigPath(), "auth.json")
c.authFilePath = filepath.Join(config.Path(), "auth.json")
sys := &containersTypes.SystemContext{
AuthFilePath: c.authFilePath,
}
@ -186,7 +186,7 @@ func NewCredentialsProvider(opts ...Opt) docker.CredentialsProvider {
return getCredentialsByCredentialHelper(dockerConfigPath, registry)
},
func(registry string) (docker.Credentials, error) {
creds, err := config.GetCredentials(sys, registry)
creds, err := dockerConfig.GetCredentials(sys, registry)
if err != nil {
return docker.Credentials{}, err
}

View File

@ -27,7 +27,7 @@ import (
"testing"
"time"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/config"
"knative.dev/kn-plugin-func/docker"
"knative.dev/kn-plugin-func/docker/creds"
. "knative.dev/kn-plugin-func/testing"
@ -536,7 +536,7 @@ func cleanUpConfigs(t *testing.T) {
t.Fatal(err)
}
os.RemoveAll(fn.ConfigPath())
os.RemoveAll(config.Path())
os.RemoveAll(filepath.Join(home, ".docker"))
}
@ -577,7 +577,7 @@ func withPopulatedFuncAuthConfig(t *testing.T) {
var err error
authConfig := filepath.Join(fn.ConfigPath(), "auth.json")
authConfig := filepath.Join(config.Path(), "auth.json")
err = os.MkdirAll(filepath.Dir(authConfig), 0700)
if err != nil {
t.Fatal(err)

View File

@ -221,3 +221,12 @@ func RunGitServer(t *testing.T, gitRoot string) (hostPort string) {
return hostPort
}
// Cwd returns the current working directory or panic if unable to determine.
func Cwd() (cwd string) {
cwd, err := os.Getwd()
if err != nil {
panic(fmt.Sprintf("Unable to determine current working directory: %v", err))
}
return cwd
}