Centralize environment configuration (#6293)

* Define central config

* Use envConfig in GenericRun for segment parameters

* Pass the env config into the context passed to CLI methods

* Use PodmanCmd and DockerCmd from context

* Remove tests now that ODO_DISABLE_TELEMETRY is checked for a bool value

* deploy.Deploy: Use values from ctx instead of parameters + use FS from DI

* dev.Start: Use values from ctx instead of parameters

* image.Build*: Use values from ctx instead of parameters

* Use telemetry file from context

* Pass ctx to segment.getTelemetryForDevfileRegistry

* Use ctx in getTelemetryForDevfileRegistry

* Call IsTelemetryEnabled once and use scontext.GetTelemetryStatus after

* Fix unit tests

* Use envConfig in segment.IsTelemetryEnabled

* Define TelemetryCaller constant in test helper

* IsTrackingConsentEnabled: get value from envconfig instead of env

* Use ctx instead of GLOBALODOCONFIG

* Place ODO_EXPERIMENTAL_MODE in configuration

* Use maintained envconfig package

* Define default values when exist

* Document accepted boolean values
This commit is contained in:
Philippe Martin
2022-11-17 18:57:34 +01:00
committed by GitHub
parent ce42ce435e
commit 712254ae66
80 changed files with 2091 additions and 451 deletions

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"errors"
"fmt"
"os"
@@ -171,11 +172,12 @@ func main() {
if len(args) == 0 {
fmt.Print(command.Usage())
} else {
ctx := context.Background()
switch args[0] {
case "reference":
fmt.Print(referencePrinter(cli.NewCmdOdo(cli.OdoRecommendedName, cli.OdoRecommendedName), 0))
fmt.Print(referencePrinter(cli.NewCmdOdo(ctx, cli.OdoRecommendedName, cli.OdoRecommendedName), 0))
case "structure":
fmt.Print(commandPrinter(cli.NewCmdOdo(cli.OdoRecommendedName, cli.OdoRecommendedName), 0))
fmt.Print(commandPrinter(cli.NewCmdOdo(ctx, cli.OdoRecommendedName, cli.OdoRecommendedName), 0))
default:
fmt.Print(command.Usage())
}

View File

@@ -6,6 +6,11 @@ import (
"os"
"github.com/posener/complete"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli"
"github.com/redhat-developer/odo/pkg/odo/cli/version"
@@ -13,16 +18,24 @@ import (
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/redhat-developer/odo/pkg/preference"
segment "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/klog"
)
func main() {
// Create a context ready for receiving telemetry data
// and save into it configuration based on environment variables
ctx := segment.NewContext(context.Background())
envConfig, err := config.GetConfiguration()
if err != nil {
util.LogErrorAndExit(err, "")
}
ctx = envcontext.WithEnvConfig(ctx, *envConfig)
// create the complete command
klog.InitFlags(nil)
root := cli.NewCmdOdo(cli.OdoRecommendedName, cli.OdoRecommendedName)
root := cli.NewCmdOdo(ctx, cli.OdoRecommendedName, cli.OdoRecommendedName)
rootCmp := createCompletion(root)
cmp := complete.New("odo", rootCmp)
@@ -41,7 +54,7 @@ func main() {
// parse the flags but hack around to avoid exiting with error code 2 on help
flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
args := os.Args[1:]
if err := flag.CommandLine.Parse(args); err != nil {
if err = flag.CommandLine.Parse(args); err != nil {
if err == flag.ErrHelp {
os.Exit(0)
}
@@ -55,7 +68,7 @@ func main() {
return
}
cfg, err := preference.NewClient()
cfg, err := preference.NewClient(ctx)
if err != nil {
util.LogErrorAndExit(err, "")
}
@@ -67,7 +80,7 @@ func main() {
updateInfo := make(chan string)
go version.GetLatestReleaseInfo(updateInfo)
util.LogErrorAndExit(root.ExecuteContext(segment.NewContext(context.Background())), "")
util.LogErrorAndExit(root.ExecuteContext(ctx), "")
select {
case message := <-updateInfo:
log.Info(message)
@@ -75,7 +88,7 @@ func main() {
klog.V(4).Info("Could not get the latest release information in time. Never mind, exiting gracefully :)")
}
} else {
util.LogErrorAndExit(root.ExecuteContext(segment.NewContext(context.Background())), "")
util.LogErrorAndExit(root.ExecuteContext(ctx), "")
}
}

View File

@@ -190,4 +190,6 @@ Options here are mostly used for debugging and testing `odo` behavior.
| `DEVFILE_PROXY` | Integration tests will use this address as Devfile registry instead of `registry.stage.devfile.io` | v3.0.0-beta3 | `my-registry.example.com` |
| `TELEMETRY_CALLER` | Caller identifier passed to [telemetry](https://github.com/redhat-developer/odo/blob/main/USAGE_DATA.md). Case-insensitive. Acceptable values: `vscode`, `intellij`, `jboss`. | v3.1.0 | `intellij` |
| `ODO_TRACKING_CONSENT` | Useful for controlling [telemetry](https://github.com/redhat-developer/odo/blob/main/USAGE_DATA.md). Acceptable values: `yes` ([enables telemetry](https://github.com/redhat-developer/odo/blob/main/USAGE_DATA.md) and skips consent prompt), `no` (disables telemetry and consent prompt). Takes precedence over the [`ConsentTelemetry`](#preference-key-table) preference. | v3.2.0 | `yes` |
| `ODO_EXPERIMENTAL_MODE` | Whether to enable experimental features. See [Experimental Mode](../user-guides/advanced/experimental-mode) for more details. Acceptable values: `true` | v3.3.0 | `true` |
| `ODO_EXPERIMENTAL_MODE` | Whether to enable experimental features. See [Experimental Mode](../user-guides/advanced/experimental-mode) for more details. Acceptable values: boolean values<sup>(1)</sup> | v3.3.0 | `true` |
(1) Accepted boolean values are: `1`, `t`, `T`, `TRUE`, `true`, `True`, `0`, `f`, `F`, `FALSE`, `false`, `False`.

1
go.mod
View File

@@ -39,6 +39,7 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/securego/gosec/v2 v2.14.0
github.com/segmentio/backo-go v1.0.1-0.20200129164019-23eae7c10bd3
github.com/sethvargo/go-envconfig v0.8.2
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5

5
go.sum
View File

@@ -815,6 +815,7 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@@ -883,8 +884,6 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
@@ -1052,6 +1051,8 @@ github.com/segmentio/backo-go v1.0.1-0.20200129164019-23eae7c10bd3/go.mod h1:9/R
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethvargo/go-envconfig v0.8.2 h1:DDUVuG21RMgeB/bn4leclUI/837y6cQCD4w8hb5797k=
github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=

View File

@@ -1,6 +1,7 @@
package alizer
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -26,9 +27,9 @@ func NewAlizerClient(registryClient registry.Client) *Alizer {
// DetectFramework uses the alizer library in order to detect the devfile
// to use depending on the files in the path
func (o *Alizer) DetectFramework(path string) (recognizer.DevFileType, api.Registry, error) {
func (o *Alizer) DetectFramework(ctx context.Context, path string) (recognizer.DevFileType, api.Registry, error) {
types := []recognizer.DevFileType{}
components, err := o.registryClient.ListDevfileStacks("", "", "", false)
components, err := o.registryClient.ListDevfileStacks(ctx, "", "", "", false)
if err != nil {
return recognizer.DevFileType{}, api.Registry{}, err
}

View File

@@ -1,6 +1,7 @@
package alizer
import (
"context"
"path/filepath"
"runtime"
"testing"
@@ -113,10 +114,11 @@ func TestDetectFramework(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
registryClient := registry.NewMockClient(ctrl)
registryClient.EXPECT().ListDevfileStacks("", "", "", false).Return(list, nil)
ctx := context.Background()
registryClient.EXPECT().ListDevfileStacks(ctx, "", "", "", false).Return(list, nil)
alizerClient := NewAlizerClient(registryClient)
// Run function DetectFramework
detected, registry, err := alizerClient.DetectFramework(tt.args.path)
detected, registry, err := alizerClient.DetectFramework(ctx, tt.args.path)
if !tt.wantErr == (err != nil) {
t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)

View File

@@ -1,11 +1,13 @@
package alizer
import (
"context"
"github.com/redhat-developer/alizer/go/pkg/apis/recognizer"
"github.com/redhat-developer/odo/pkg/api"
)
type Client interface {
DetectFramework(path string) (recognizer.DevFileType, api.Registry, error)
DetectFramework(ctx context.Context, path string) (recognizer.DevFileType, api.Registry, error)
DetectName(path string) (string, error)
}

View File

@@ -5,6 +5,7 @@
package alizer
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
@@ -36,9 +37,9 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// DetectFramework mocks base method.
func (m *MockClient) DetectFramework(path string) (recognizer.DevFileType, api.Registry, error) {
func (m *MockClient) DetectFramework(ctx context.Context, path string) (recognizer.DevFileType, api.Registry, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DetectFramework", path)
ret := m.ctrl.Call(m, "DetectFramework", ctx, path)
ret0, _ := ret[0].(recognizer.DevFileType)
ret1, _ := ret[1].(api.Registry)
ret2, _ := ret[2].(error)
@@ -46,9 +47,9 @@ func (m *MockClient) DetectFramework(path string) (recognizer.DevFileType, api.R
}
// DetectFramework indicates an expected call of DetectFramework.
func (mr *MockClientMockRecorder) DetectFramework(path interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) DetectFramework(ctx, path interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetectFramework", reflect.TypeOf((*MockClient)(nil).DetectFramework), path)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetectFramework", reflect.TypeOf((*MockClient)(nil).DetectFramework), ctx, path)
}
// DetectName mocks base method.

29
pkg/config/config.go Normal file
View File

@@ -0,0 +1,29 @@
package config
import (
"context"
"github.com/sethvargo/go-envconfig"
)
type Configuration struct {
DevfileProxy *string `env:"DEVFILE_PROXY,noinit"`
DockerCmd string `env:"DOCKER_CMD,default=docker"`
Globalodoconfig *string `env:"GLOBALODOCONFIG,noinit"`
OdoDebugTelemetryFile *string `env:"ODO_DEBUG_TELEMETRY_FILE,noinit"`
OdoDisableTelemetry *bool `env:"ODO_DISABLE_TELEMETRY,noinit"`
OdoLogLevel *int `env:"ODO_LOG_LEVEL,noinit"`
OdoTrackingConsent *string `env:"ODO_TRACKING_CONSENT,noinit"`
PodmanCmd string `env:"PODMAN_CMD,default=podman"`
TelemetryCaller string `env:"TELEMETRY_CALLER,default="`
OdoExperimentalMode bool `env:"ODO_EXPERIMENTAL_MODE,default=false"`
}
func GetConfiguration() (*Configuration, error) {
var s Configuration
err := envconfig.Process(context.Background(), &s)
if err != nil {
return nil, err
}
return &s, nil
}

54
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,54 @@
package config
import (
"testing"
)
func TestDefaultValues(t *testing.T) {
cfg, err := GetConfiguration()
if err != nil {
t.Errorf("Error is not expected: %v", err)
}
checkDefaultStringValue(t, "DockerCmd", cfg.DockerCmd, "docker")
checkDefaultStringValue(t, "PodmanCmd", cfg.PodmanCmd, "podman")
checkDefaultStringValue(t, "TelemetryCaller", cfg.TelemetryCaller, "")
checkDefaultBoolValue(t, "OdoExperimentalMode", cfg.OdoExperimentalMode, false)
// Use noinit to set non initialized value as nil instead of zero-value
checkNilString(t, "DevfileProxy", cfg.DevfileProxy)
checkNilString(t, "Globalodoconfig", cfg.Globalodoconfig)
checkNilString(t, "Globalodoconfig", cfg.Globalodoconfig)
checkNilString(t, "OdoDebugTelemetryFile", cfg.OdoDebugTelemetryFile)
checkNilBool(t, "OdoDisableTelemetry", cfg.OdoDisableTelemetry)
checkNilString(t, "OdoTrackingConsent", cfg.OdoTrackingConsent)
}
func checkDefaultStringValue(t *testing.T, fieldName string, field string, def string) {
if field != def {
t.Errorf("default value for %q should be %q but is %q", fieldName, def, field)
}
}
func checkDefaultBoolValue(t *testing.T, fieldName string, field bool, def bool) {
if field != def {
t.Errorf("default value for %q should be %v but is %v", fieldName, def, field)
}
}
func checkNilString(t *testing.T, fieldName string, field *string) {
if field != nil {
t.Errorf("value for non specified env var %q should be nil but is %q", fieldName, *field)
}
}
func checkNilBool(t *testing.T, fieldName string, field *bool) {
if field != nil {
t.Errorf("value for non specified env var %q should be nil but is %v", fieldName, *field)
}
}

View File

@@ -0,0 +1,25 @@
package context
import (
"context"
"github.com/redhat-developer/odo/pkg/config"
)
type contextKey struct{}
var key = contextKey{}
// WithEnvConfig sets the environment configuration in ctx
func WithEnvConfig(ctx context.Context, val config.Configuration) context.Context {
return context.WithValue(ctx, key, val)
}
// GetEnvConfig returns the environment configuration from ctx
func GetEnvConfig(ctx context.Context) config.Configuration {
value := ctx.Value(key)
if cast, ok := value.(config.Configuration); ok {
return cast
}
panic("GetEnvConfig can be called only after WithEnvConfig has been called")
}

View File

@@ -1,7 +1,9 @@
package deploy
import (
"context"
"errors"
"path/filepath"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
@@ -11,27 +13,38 @@ import (
"github.com/redhat-developer/odo/pkg/kclient"
odolabels "github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
type DeployClient struct {
kubeClient kclient.ClientInterface
fs filesystem.Filesystem
}
var _ Client = (*DeployClient)(nil)
func NewDeployClient(kubeClient kclient.ClientInterface) *DeployClient {
func NewDeployClient(kubeClient kclient.ClientInterface, fs filesystem.Filesystem) *DeployClient {
return &DeployClient{
kubeClient: kubeClient,
fs: fs,
}
}
func (o *DeployClient) Deploy(fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, appName string, componentName string) error {
deployHandler := newDeployHandler(fs, devfileObj, path, o.kubeClient, appName, componentName)
return libdevfile.Deploy(devfileObj, deployHandler)
func (o *DeployClient) Deploy(ctx context.Context) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)
appName = odocontext.GetApplication(ctx)
)
deployHandler := newDeployHandler(ctx, o.fs, *devfileObj, path, o.kubeClient, appName, componentName)
return libdevfile.Deploy(*devfileObj, deployHandler)
}
type deployHandler struct {
ctx context.Context
fs filesystem.Filesystem
devfileObj parser.DevfileObj
path string
@@ -42,8 +55,9 @@ type deployHandler struct {
var _ libdevfile.Handler = (*deployHandler)(nil)
func newDeployHandler(fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, kubeClient kclient.ClientInterface, appName string, componentName string) *deployHandler {
func newDeployHandler(ctx context.Context, fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, kubeClient kclient.ClientInterface, appName string, componentName string) *deployHandler {
return &deployHandler{
ctx: ctx,
fs: fs,
devfileObj: devfileObj,
path: path,
@@ -55,7 +69,7 @@ func newDeployHandler(fs filesystem.Filesystem, devfileObj parser.DevfileObj, pa
// ApplyImage builds and pushes the OCI image to be used on Kubernetes
func (o *deployHandler) ApplyImage(img v1alpha2.Component) error {
return image.BuildPushSpecificImage(o.fs, o.path, img, true)
return image.BuildPushSpecificImage(o.ctx, o.fs, img, true)
}
// ApplyKubernetes applies inline Kubernetes YAML from the devfile.yaml file

View File

@@ -1,14 +1,12 @@
package deploy
import (
"github.com/devfile/library/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
"context"
)
type Client interface {
// Deploy resources from a devfile located in path, for the specified appName.
// The filesystem specified is used to download and store the Dockerfiles needed to build the necessary container images,
// in case such Dockerfiles are referenced as remote URLs in the Devfile.
Deploy(fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, appName string, componentName string) error
Deploy(ctx context.Context) error
}

View File

@@ -5,11 +5,10 @@
package deploy
import (
context "context"
reflect "reflect"
parser "github.com/devfile/library/pkg/devfile/parser"
gomock "github.com/golang/mock/gomock"
filesystem "github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
// MockClient is a mock of Client interface.
@@ -36,15 +35,15 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// Deploy mocks base method.
func (m *MockClient) Deploy(fs filesystem.Filesystem, devfileObj parser.DevfileObj, path, appName, componentName string) error {
func (m *MockClient) Deploy(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deploy", fs, devfileObj, path, appName, componentName)
ret := m.ctrl.Call(m, "Deploy", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Deploy indicates an expected call of Deploy.
func (mr *MockClientMockRecorder) Deploy(fs, devfileObj, path, appName, componentName interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) Deploy(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockClient)(nil).Deploy), fs, devfileObj, path, appName, componentName)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockClient)(nil).Deploy), ctx)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"path/filepath"
"github.com/redhat-developer/odo/pkg/binding"
"github.com/redhat-developer/odo/pkg/devfile"
@@ -14,7 +15,6 @@ import (
"github.com/redhat-developer/odo/pkg/sync"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
"github.com/devfile/library/pkg/devfile/parser"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
@@ -61,23 +61,26 @@ func NewDevClient(
func (o *DevClient) Start(
ctx context.Context,
devfileObj parser.DevfileObj,
componentName string,
path string,
devfilePath string,
out io.Writer,
errOut io.Writer,
options StartOptions,
) error {
klog.V(4).Infoln("Creating new adapter")
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)
)
adapter := component.NewKubernetesAdapter(
o.kubernetesClient, o.prefClient, o.portForwardClient, o.bindingClient, o.syncClient, o.execClient,
component.AdapterContext{
ComponentName: componentName,
Context: path,
AppName: odocontext.GetApplication(ctx),
Devfile: devfileObj,
Devfile: *devfileObj,
FS: o.filesystem,
})
@@ -93,7 +96,7 @@ func (o *DevClient) Start(
klog.V(4).Infoln("Creating inner-loop resources for the component")
componentStatus := watch.ComponentStatus{}
err := adapter.Push(pushParameters, &componentStatus)
err := adapter.Push(ctx, pushParameters, &componentStatus)
if err != nil {
return err
}
@@ -106,7 +109,7 @@ func (o *DevClient) Start(
ApplicationName: odocontext.GetApplication(ctx),
DevfileWatchHandler: o.regenerateAdapterAndPush,
FileIgnores: options.IgnorePaths,
InitialDevfileObj: devfileObj,
InitialDevfileObj: *devfileObj,
Debug: options.Debug,
DevfileBuildCmd: options.BuildCommand,
DevfileRunCmd: options.RunCommand,
@@ -120,7 +123,7 @@ func (o *DevClient) Start(
}
// RegenerateAdapterAndPush regenerates the adapter and pushes the files to remote pod
func (o *DevClient) regenerateAdapterAndPush(pushParams adapters.PushParameters, watchParams watch.WatchParameters, componentStatus *watch.ComponentStatus) error {
func (o *DevClient) regenerateAdapterAndPush(ctx context.Context, pushParams adapters.PushParameters, watchParams watch.WatchParameters, componentStatus *watch.ComponentStatus) error {
var adapter component.ComponentAdapter
adapter, err := o.regenerateComponentAdapterFromWatchParams(watchParams)
@@ -128,7 +131,7 @@ func (o *DevClient) regenerateAdapterAndPush(pushParams adapters.PushParameters,
return fmt.Errorf("unable to generate component from watch parameters: %w", err)
}
err = adapter.Push(pushParams, componentStatus)
err = adapter.Push(ctx, pushParams, componentStatus)
if err != nil {
return fmt.Errorf("watch command was unable to push component: %w", err)
}

View File

@@ -3,8 +3,6 @@ package dev
import (
"context"
"io"
"github.com/devfile/library/pkg/devfile/parser"
)
type StartOptions struct {
@@ -30,10 +28,6 @@ type Client interface {
// It logs messages and errors to out and errOut.
Start(
ctx context.Context,
devfileObj parser.DevfileObj,
componentName string,
path string,
devfilePath string,
out io.Writer,
errOut io.Writer,
options StartOptions,

View File

@@ -9,7 +9,6 @@ import (
io "io"
reflect "reflect"
parser "github.com/devfile/library/pkg/devfile/parser"
gomock "github.com/golang/mock/gomock"
)
@@ -37,15 +36,15 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// Start mocks base method.
func (m *MockClient) Start(ctx context.Context, devfileObj parser.DevfileObj, componentName, path, devfilePath string, out, errOut io.Writer, options StartOptions) error {
func (m *MockClient) Start(ctx context.Context, out, errOut io.Writer, options StartOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Start", ctx, devfileObj, componentName, path, devfilePath, out, errOut, options)
ret := m.ctrl.Call(m, "Start", ctx, out, errOut, options)
ret0, _ := ret[0].(error)
return ret0
}
// Start indicates an expected call of Start.
func (mr *MockClientMockRecorder) Start(ctx, devfileObj, componentName, path, devfilePath, out, errOut, options interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) Start(ctx, out, errOut, options interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start), ctx, devfileObj, componentName, path, devfilePath, out, errOut, options)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start), ctx, out, errOut, options)
}

View File

@@ -1,6 +1,7 @@
package component
import (
"context"
"errors"
"fmt"
"path/filepath"
@@ -89,7 +90,7 @@ func NewKubernetesAdapter(
// Push updates the component if a matching component exists or creates one if it doesn't exist
// Once the component has started, it will sync the source code to it.
// The componentStatus will be modified to reflect the status of the component when the function returns
func (a Adapter) Push(parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) (err error) {
func (a Adapter) Push(ctx context.Context, parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) (err error) {
// preliminary checks
err = dfutil.ValidateK8sResourceName("component name", a.ComponentName)
@@ -260,6 +261,7 @@ func (a Adapter) Push(parameters adapters.PushParameters, componentStatus *watch
devfile: a.Devfile,
path: parameters.Path,
podName: pod.GetName(),
ctx: ctx,
}
if commandType == devfilev1.ExecCommandType {

View File

@@ -1,6 +1,8 @@
package component
import (
"context"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/component"
@@ -23,12 +25,14 @@ type runHandler struct {
path string
componentExists bool
podName string
ctx context.Context
}
var _ libdevfile.Handler = (*runHandler)(nil)
func (a *runHandler) ApplyImage(img devfilev1.Component) error {
return image.BuildPushSpecificImage(a.fs, a.path, img, true)
return image.BuildPushSpecificImage(a.ctx, a.fs, img, true)
}
func (a *runHandler) ApplyKubernetes(kubernetes devfilev1.Component) error {

View File

@@ -1,11 +1,13 @@
package component
import (
"context"
"github.com/redhat-developer/odo/pkg/devfile/adapters"
"github.com/redhat-developer/odo/pkg/watch"
)
// ComponentAdapter defines the functions that platform-specific adapters must implement
type ComponentAdapter interface {
Push(parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) error
Push(ctx context.Context, parameters adapters.PushParameters, componentStatus *watch.ComponentStatus) error
}

View File

@@ -2,16 +2,18 @@
package image
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/libdevfile"
"github.com/redhat-developer/odo/pkg/log"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
@@ -27,13 +29,17 @@ type Backend interface {
}
var lookPathCmd = exec.LookPath
var getEnvFunc = os.Getenv
// BuildPushImages build all images defined in the devfile with the detected backend
// If push is true, also push the images to their registries
func BuildPushImages(fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, push bool) error {
func BuildPushImages(ctx context.Context, fs filesystem.Filesystem, push bool) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
)
backend, err := selectBackend()
backend, err := selectBackend(ctx)
if err != nil {
return err
}
@@ -59,13 +65,16 @@ func BuildPushImages(fs filesystem.Filesystem, devfileObj parser.DevfileObj, pat
// BuildPushSpecificImage build an image defined in the devfile present in devfilePath
// If push is true, also push the image to its registry
func BuildPushSpecificImage(fs filesystem.Filesystem, devfilePath string, component devfile.Component, push bool) error {
backend, err := selectBackend()
func BuildPushSpecificImage(ctx context.Context, fs filesystem.Filesystem, component devfile.Component, push bool) error {
var (
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
)
backend, err := selectBackend(ctx)
if err != nil {
return err
}
return buildPushImage(backend, fs, component.Image, devfilePath, push)
return buildPushImage(backend, fs, component.Image, path, push)
}
// buildPushImage build an image using the provided backend
@@ -91,12 +100,9 @@ func buildPushImage(backend Backend, fs filesystem.Filesystem, image *devfile.Im
// selectBackend selects the container backend to use for building and pushing images
// It will detect podman and docker CLIs (in this order),
// or return an error if none are present locally
func selectBackend() (Backend, error) {
func selectBackend(ctx context.Context) (Backend, error) {
podmanCmd := getEnvFunc("PODMAN_CMD")
if podmanCmd == "" {
podmanCmd = "podman"
}
podmanCmd := envcontext.GetEnvConfig(ctx).PodmanCmd
if _, err := lookPathCmd(podmanCmd); err == nil {
// Podman does NOT build x86 images on Apple Silicon / M1 and we must *WARN* the user that this will not work.
@@ -116,10 +122,7 @@ func selectBackend() (Backend, error) {
return NewDockerCompatibleBackend(podmanCmd), nil
}
dockerCmd := getEnvFunc("DOCKER_CMD")
if dockerCmd == "" {
dockerCmd = "docker"
}
dockerCmd := envcontext.GetEnvConfig(ctx).DockerCmd
if _, err := lookPathCmd(dockerCmd); err == nil {
return NewDockerCompatibleBackend(dockerCmd), nil
}

View File

@@ -1,14 +1,16 @@
package image
import (
"context"
"errors"
"os"
"os/exec"
"testing"
devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
gomock "github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
@@ -120,13 +122,17 @@ func TestBuildPushImage(t *testing.T) {
func TestSelectBackend(t *testing.T) {
tests := []struct {
name string
getEnvFunc func(string) string
envConfig config.Configuration
lookPathCmd func(string) (string, error)
wantType string
wantErr bool
}{
{
name: "all backends are present",
envConfig: config.Configuration{
DockerCmd: "docker",
PodmanCmd: "podman",
},
lookPathCmd: func(string) (string, error) {
return "", nil
},
@@ -135,6 +141,10 @@ func TestSelectBackend(t *testing.T) {
},
{
name: "no backend are present",
envConfig: config.Configuration{
DockerCmd: "docker",
PodmanCmd: "podman",
},
lookPathCmd: func(string) (string, error) {
return "", errors.New("")
},
@@ -142,6 +152,10 @@ func TestSelectBackend(t *testing.T) {
},
{
name: "only docker is present",
envConfig: config.Configuration{
DockerCmd: "docker",
PodmanCmd: "podman",
},
lookPathCmd: func(name string) (string, error) {
if name == "docker" {
return "docker", nil
@@ -153,6 +167,10 @@ func TestSelectBackend(t *testing.T) {
},
{
name: "only podman is present",
envConfig: config.Configuration{
DockerCmd: "docker",
PodmanCmd: "podman",
},
lookPathCmd: func(name string) (string, error) {
if name == "podman" {
return "podman", nil
@@ -164,11 +182,9 @@ func TestSelectBackend(t *testing.T) {
},
{
name: "value of PODMAN_CMD envvar is returned if it points to a valid command",
getEnvFunc: func(name string) string {
if name == "PODMAN_CMD" {
return "my-alternate-podman-command"
}
return ""
envConfig: config.Configuration{
DockerCmd: "docker",
PodmanCmd: "my-alternate-podman-command",
},
lookPathCmd: func(name string) (string, error) {
if name == "my-alternate-podman-command" {
@@ -181,11 +197,9 @@ func TestSelectBackend(t *testing.T) {
},
{
name: "docker if PODMAN_CMD points to an invalid command",
getEnvFunc: func(name string) string {
if name == "PODMAN_CMD" {
return "no-such-command"
}
return ""
envConfig: config.Configuration{
PodmanCmd: "no-such-command",
DockerCmd: "docker",
},
lookPathCmd: func(name string) (string, error) {
if name == "docker" {
@@ -200,18 +214,11 @@ func TestSelectBackend(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.getEnvFunc != nil {
getEnvFunc = tt.getEnvFunc
} else {
getEnvFunc = func(string) string {
//Empty environment
return ""
}
}
defer func() { getEnvFunc = os.Getenv }()
lookPathCmd = tt.lookPathCmd
defer func() { lookPathCmd = exec.LookPath }()
backend, err := selectBackend()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, tt.envConfig)
backend, err := selectBackend(ctx)
if tt.wantErr != (err != nil) {
t.Errorf("%s: Error result wanted %v, got %v", tt.name, tt.wantErr, err != nil)
}

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"fmt"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -30,8 +31,8 @@ func (o *AlizerBackend) Validate(flags map[string]string, fs filesystem.Filesyst
}
// SelectDevfile calls thz Alizer to detect the devfile and asks for confirmation to the user
func (o *AlizerBackend) SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DevfileLocation, err error) {
selected, registry, err := o.alizerClient.DetectFramework(dir)
func (o *AlizerBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DevfileLocation, err error) {
selected, registry, err := o.alizerClient.DetectFramework(ctx, dir)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"path/filepath"
"reflect"
"runtime"
@@ -49,7 +50,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any()).Return(recognizer.DevFileType{
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(recognizer.DevFileType{
Name: "a-devfile-name",
}, api.Registry{
Name: "a-registry",
@@ -76,7 +77,7 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
},
alizerClient: func(ctrl *gomock.Controller) alizer.Client {
alizerClient := alizer.NewMockClient(ctrl)
alizerClient.EXPECT().DetectFramework(gomock.Any()).Return(recognizer.DevFileType{}, api.Registry{}, nil)
alizerClient.EXPECT().DetectFramework(gomock.Any(), gomock.Any()).Return(recognizer.DevFileType{}, api.Registry{}, nil)
return alizerClient
},
},
@@ -95,7 +96,8 @@ func TestAlizerBackend_SelectDevfile(t *testing.T) {
askerClient: tt.fields.askerClient(ctrl),
alizerClient: tt.fields.alizerClient(ctrl),
}
gotLocation, err := o.SelectDevfile(tt.args.flags, tt.args.fs, tt.args.dir)
ctx := context.Background()
gotLocation, err := o.SelectDevfile(ctx, tt.args.flags, tt.args.fs, tt.args.dir)
if (err != nil) != tt.wantErr {
t.Errorf("AlizerBackend.SelectDevfile() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"errors"
"fmt"
@@ -85,7 +86,7 @@ func (o *FlagsBackend) Validate(flags map[string]string, fs filesystem.Filesyste
return nil
}
func (o *FlagsBackend) SelectDevfile(flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) {
func (o *FlagsBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) {
return &api.DevfileLocation{
Devfile: flags[FLAG_DEVFILE],
DevfileRegistry: flags[FLAG_DEVFILE_REGISTRY],

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"reflect"
"testing"
@@ -47,7 +48,8 @@ func TestFlagsBackend_SelectDevfile(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &FlagsBackend{}
got, err := o.SelectDevfile(tt.fields.flags, nil, "")
ctx := context.Background()
got, err := o.SelectDevfile(ctx, tt.fields.flags, nil, "")
if (err != nil) != tt.wantErr {
t.Errorf("FlagsBackend.SelectDevfile() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"fmt"
"path/filepath"
"sort"
@@ -47,9 +48,9 @@ func (o *InteractiveBackend) Validate(flags map[string]string, fs filesystem.Fil
return nil
}
func (o *InteractiveBackend) SelectDevfile(flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) {
func (o *InteractiveBackend) SelectDevfile(ctx context.Context, flags map[string]string, _ filesystem.Filesystem, _ string) (*api.DevfileLocation, error) {
result := &api.DevfileLocation{}
devfileEntries, _ := o.registryClient.ListDevfileStacks("", "", "", false)
devfileEntries, _ := o.registryClient.ListDevfileStacks(ctx, "", "", "", false)
langs := devfileEntries.GetLanguages()
state := STATE_ASK_LANG

View File

@@ -1,6 +1,7 @@
package backend
import (
"context"
"reflect"
"testing"
@@ -46,7 +47,7 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
},
buildCatalogClient: func(ctrl *gomock.Controller) registry.Client {
client := registry.NewMockClient(ctrl)
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
return client
},
},
@@ -73,7 +74,7 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
},
buildCatalogClient: func(ctrl *gomock.Controller) registry.Client {
client := registry.NewMockClient(ctrl)
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
client.EXPECT().ListDevfileStacks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
return client
},
},
@@ -90,7 +91,8 @@ func TestInteractiveBackend_SelectDevfile(t *testing.T) {
askerClient: tt.fields.buildAsker(ctrl),
registryClient: tt.fields.buildCatalogClient(ctrl),
}
got, err := o.SelectDevfile(map[string]string{}, nil, "")
ctx := context.Background()
got, err := o.SelectDevfile(ctx, map[string]string{}, nil, "")
if (err != nil) != tt.wantErr {
t.Errorf("InteractiveBuilder.ParamsBuild() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@@ -4,6 +4,8 @@
package backend
import (
"context"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/api"
@@ -16,7 +18,7 @@ type InitBackend interface {
Validate(flags map[string]string, fs filesystem.Filesystem, dir string) error
// SelectDevfile selects a devfile and returns its location information, depending on the flags
SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DevfileLocation, err error)
SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (location *api.DevfileLocation, err error)
// SelectStarterProject selects a starter project from the devfile and returns information about the starter project,
// depending on the flags. If not starter project is selected, a nil starter is returned

View File

@@ -5,6 +5,7 @@
package backend
import (
context "context"
reflect "reflect"
v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -68,18 +69,18 @@ func (mr *MockInitBackendMockRecorder) PersonalizeName(devfile, flags interface{
}
// SelectDevfile mocks base method.
func (m *MockInitBackend) SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
func (m *MockInitBackend) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SelectDevfile", flags, fs, dir)
ret := m.ctrl.Call(m, "SelectDevfile", ctx, flags, fs, dir)
ret0, _ := ret[0].(*api.DevfileLocation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SelectDevfile indicates an expected call of SelectDevfile.
func (mr *MockInitBackendMockRecorder) SelectDevfile(flags, fs, dir interface{}) *gomock.Call {
func (mr *MockInitBackendMockRecorder) SelectDevfile(ctx, flags, fs, dir interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectDevfile", reflect.TypeOf((*MockInitBackend)(nil).SelectDevfile), flags, fs, dir)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectDevfile", reflect.TypeOf((*MockInitBackend)(nil).SelectDevfile), ctx, flags, fs, dir)
}
// SelectStarterProject mocks base method.

View File

@@ -1,6 +1,7 @@
package init
import (
"context"
"errors"
"fmt"
"net/url"
@@ -76,7 +77,7 @@ func (o *InitClient) Validate(flags map[string]string, fs filesystem.Filesystem,
}
// SelectDevfile calls SelectDevfile methods of the adequate backend
func (o *InitClient) SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
func (o *InitClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
var backend backend.InitBackend
empty, err := location.DirIsEmpty(fs, dir)
@@ -90,7 +91,7 @@ func (o *InitClient) SelectDevfile(flags map[string]string, fs filesystem.Filesy
} else {
backend = o.flagsBackend
}
location, err := backend.SelectDevfile(flags, fs, dir)
location, err := backend.SelectDevfile(ctx, flags, fs, dir)
if err != nil {
return nil, err
}
@@ -99,7 +100,7 @@ func (o *InitClient) SelectDevfile(flags map[string]string, fs filesystem.Filesy
if location == nil {
if backend == o.alizerBackend {
backend = o.interactiveBackend
return backend.SelectDevfile(flags, fs, dir)
return backend.SelectDevfile(ctx, flags, fs, dir)
} else {
return nil, errors.New("unable to determine the devfile location")
}
@@ -108,12 +109,12 @@ func (o *InitClient) SelectDevfile(flags map[string]string, fs filesystem.Filesy
return location, err
}
func (o *InitClient) DownloadDevfile(devfileLocation *api.DevfileLocation, destDir string) (string, error) {
func (o *InitClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error) {
destDevfile := filepath.Join(destDir, "devfile.yaml")
if devfileLocation.DevfilePath != "" {
return destDevfile, o.downloadDirect(devfileLocation.DevfilePath, destDevfile)
} else {
return destDevfile, o.downloadFromRegistry(devfileLocation.DevfileRegistry, devfileLocation.Devfile, destDir)
return destDevfile, o.downloadFromRegistry(ctx, devfileLocation.DevfileRegistry, devfileLocation.Devfile, destDir)
}
}
@@ -161,7 +162,7 @@ func (o *InitClient) downloadDirect(URL string, dest string) error {
// downloadFromRegistry downloads a devfile from the provided registry and saves it in dest
// If registryName is empty, will try to download the devfile from the list of registries in preferences
func (o *InitClient) downloadFromRegistry(registryName string, devfile string, dest string) error {
func (o *InitClient) downloadFromRegistry(ctx context.Context, registryName string, devfile string, dest string) error {
var downloadSpinner *log.Status
var forceRegistry bool
if registryName == "" {
@@ -177,14 +178,14 @@ func (o *InitClient) downloadFromRegistry(registryName string, devfile string, d
var reg preference.Registry
for _, reg = range registries {
if forceRegistry && reg.Name == registryName {
err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, segment.GetRegistryOptions())
err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, segment.GetRegistryOptions(ctx))
if err != nil {
return err
}
downloadSpinner.End(true)
return nil
} else if !forceRegistry {
err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, segment.GetRegistryOptions())
err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, segment.GetRegistryOptions(ctx))
if err != nil {
continue
}
@@ -254,13 +255,13 @@ func (o InitClient) PersonalizeDevfileConfig(devfileobj parser.DevfileObj, flags
return backend.PersonalizeDevfileConfig(devfileobj)
}
func (o InitClient) SelectAndPersonalizeDevfile(flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) {
devfileLocation, err := o.SelectDevfile(flags, o.fsys, contextDir)
func (o InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) {
devfileLocation, err := o.SelectDevfile(ctx, flags, o.fsys, contextDir)
if err != nil {
return parser.DevfileObj{}, "", nil, err
}
devfilePath, err := o.DownloadDevfile(devfileLocation, contextDir)
devfilePath, err := o.DownloadDevfile(ctx, devfileLocation, contextDir)
if err != nil {
return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to download devfile: %w", err)
}
@@ -277,7 +278,7 @@ func (o InitClient) SelectAndPersonalizeDevfile(flags map[string]string, context
return devfileObj, devfilePath, devfileLocation, nil
}
func (o InitClient) InitDevfile(flags map[string]string, contextDir string,
func (o InitClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string,
preInitHandlerFunc func(interactiveMode bool), newDevfileHandlerFunc func(newDevfileObj parser.DevfileObj) error) error {
containsDevfile, err := location.DirectoryContainsDevfile(o.fsys, contextDir)
@@ -292,7 +293,7 @@ func (o InitClient) InitDevfile(flags map[string]string, contextDir string,
preInitHandlerFunc(len(flags) == 0)
}
devfileObj, _, _, err := o.SelectAndPersonalizeDevfile(map[string]string{}, contextDir)
devfileObj, _, _, err := o.SelectAndPersonalizeDevfile(ctx, map[string]string{}, contextDir)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package init
import (
"context"
"errors"
"testing"
@@ -8,6 +9,8 @@ import (
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/registry"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
@@ -163,7 +166,9 @@ func TestInitClient_downloadFromRegistry(t *testing.T) {
preferenceClient: tt.fields.preferenceClient(ctrl),
registryClient: tt.fields.registryClient(ctrl),
}
if err := o.downloadFromRegistry(tt.args.registryName, tt.args.devfile, tt.args.dest); (err != nil) != tt.wantErr {
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
if err := o.downloadFromRegistry(ctx, tt.args.registryName, tt.args.devfile, tt.args.dest); (err != nil) != tt.wantErr {
t.Errorf("InitClient.downloadFromRegistry() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@@ -8,6 +8,8 @@
package init
import (
"context"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
@@ -29,17 +31,17 @@ type Client interface {
// `newDevfileHandlerFunc` is called only when a new Devfile object has been instantiated.
// It allows to perform operations right after the Devfile has been initialized and personalized.
// It is not called if the context directory already has a Devfile file.
InitDevfile(flags map[string]string, contextDir string, preInitHandlerFunc func(interactiveMode bool),
InitDevfile(ctx context.Context, flags map[string]string, contextDir string, preInitHandlerFunc func(interactiveMode bool),
newDevfileHandlerFunc func(newDevfileObj parser.DevfileObj) error) error
// SelectDevfile returns information about a devfile selected based on Alizer if the directory content,
// or based on the flags if the directory is empty, or
// interactively if flags is empty
SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error)
SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error)
// DownloadDevfile downloads a devfile given its location information and a destination directory
// and returns the path of the downloaded file
DownloadDevfile(devfileLocation *api.DevfileLocation, destDir string) (string, error)
DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error)
// SelectStarterProject selects a starter project from the devfile and returns information about the starter project,
// depending on the flags. If not starter project is selected, a nil starter is returned
@@ -58,5 +60,5 @@ type Client interface {
// SelectAndPersonalizeDevfile selects a devfile, then downloads, parse and personalize it
// Returns the devfile object, its path and pointer to *api.devfileLocation
SelectAndPersonalizeDevfile(flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error)
SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error)
}

View File

@@ -5,6 +5,7 @@
package init
import (
context "context"
reflect "reflect"
v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -38,18 +39,18 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
}
// DownloadDevfile mocks base method.
func (m *MockClient) DownloadDevfile(devfileLocation *api.DevfileLocation, destDir string) (string, error) {
func (m *MockClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DevfileLocation, destDir string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DownloadDevfile", devfileLocation, destDir)
ret := m.ctrl.Call(m, "DownloadDevfile", ctx, devfileLocation, destDir)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DownloadDevfile indicates an expected call of DownloadDevfile.
func (mr *MockClientMockRecorder) DownloadDevfile(devfileLocation, destDir interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) DownloadDevfile(ctx, devfileLocation, destDir interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadDevfile", reflect.TypeOf((*MockClient)(nil).DownloadDevfile), devfileLocation, destDir)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadDevfile", reflect.TypeOf((*MockClient)(nil).DownloadDevfile), ctx, devfileLocation, destDir)
}
// DownloadStarterProject mocks base method.
@@ -81,17 +82,17 @@ func (mr *MockClientMockRecorder) GetFlags(flags interface{}) *gomock.Call {
}
// InitDevfile mocks base method.
func (m *MockClient) InitDevfile(flags map[string]string, contextDir string, preInitHandlerFunc func(bool), newDevfileHandlerFunc func(parser.DevfileObj) error) error {
func (m *MockClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string, preInitHandlerFunc func(bool), newDevfileHandlerFunc func(parser.DevfileObj) error) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InitDevfile", flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc)
ret := m.ctrl.Call(m, "InitDevfile", ctx, flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc)
ret0, _ := ret[0].(error)
return ret0
}
// InitDevfile indicates an expected call of InitDevfile.
func (mr *MockClientMockRecorder) InitDevfile(flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) InitDevfile(ctx, flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitDevfile", reflect.TypeOf((*MockClient)(nil).InitDevfile), flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitDevfile", reflect.TypeOf((*MockClient)(nil).InitDevfile), ctx, flags, contextDir, preInitHandlerFunc, newDevfileHandlerFunc)
}
// PersonalizeDevfileConfig mocks base method.
@@ -125,9 +126,9 @@ func (mr *MockClientMockRecorder) PersonalizeName(devfile, flags interface{}) *g
}
// SelectAndPersonalizeDevfile mocks base method.
func (m *MockClient) SelectAndPersonalizeDevfile(flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) {
func (m *MockClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DevfileLocation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SelectAndPersonalizeDevfile", flags, contextDir)
ret := m.ctrl.Call(m, "SelectAndPersonalizeDevfile", ctx, flags, contextDir)
ret0, _ := ret[0].(parser.DevfileObj)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(*api.DevfileLocation)
@@ -136,24 +137,24 @@ func (m *MockClient) SelectAndPersonalizeDevfile(flags map[string]string, contex
}
// SelectAndPersonalizeDevfile indicates an expected call of SelectAndPersonalizeDevfile.
func (mr *MockClientMockRecorder) SelectAndPersonalizeDevfile(flags, contextDir interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) SelectAndPersonalizeDevfile(ctx, flags, contextDir interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectAndPersonalizeDevfile", reflect.TypeOf((*MockClient)(nil).SelectAndPersonalizeDevfile), flags, contextDir)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectAndPersonalizeDevfile", reflect.TypeOf((*MockClient)(nil).SelectAndPersonalizeDevfile), ctx, flags, contextDir)
}
// SelectDevfile mocks base method.
func (m *MockClient) SelectDevfile(flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
func (m *MockClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DevfileLocation, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SelectDevfile", flags, fs, dir)
ret := m.ctrl.Call(m, "SelectDevfile", ctx, flags, fs, dir)
ret0, _ := ret[0].(*api.DevfileLocation)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SelectDevfile indicates an expected call of SelectDevfile.
func (mr *MockClientMockRecorder) SelectDevfile(flags, fs, dir interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) SelectDevfile(ctx, flags, fs, dir interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectDevfile", reflect.TypeOf((*MockClient)(nil).SelectDevfile), flags, fs, dir)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectDevfile", reflect.TypeOf((*MockClient)(nil).SelectDevfile), ctx, flags, fs, dir)
}
// SelectStarterProject mocks base method.

View File

@@ -49,7 +49,7 @@ func (o *AlizerOptions) Run(ctx context.Context) (err error) {
// Run contains the logic for the odo command
func (o *AlizerOptions) RunForJsonOutput(ctx context.Context) (out interface{}, err error) {
workingDir := odocontext.GetWorkingDirectory(ctx)
df, reg, err := o.clientset.AlizerClient.DetectFramework(workingDir)
df, reg, err := o.clientset.AlizerClient.DetectFramework(ctx, workingDir)
if err != nil {
return nil, err
}

View File

@@ -3,7 +3,6 @@ package build_images
import (
"context"
"fmt"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"
@@ -63,12 +62,7 @@ func (o *BuildImagesOptions) Validate(ctx context.Context) (err error) {
// Run contains the logic for the odo command
func (o *BuildImagesOptions) Run(ctx context.Context) (err error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
)
return image.BuildPushImages(o.clientset.FS, *devfileObj, path, o.pushFlag)
return image.BuildPushImages(ctx, o.clientset.FS, o.pushFlag)
}
// NewCmdBuildImages implements the odo command

View File

@@ -1,6 +1,7 @@
package cli
import (
"context"
"errors"
"flag"
"fmt"
@@ -101,8 +102,8 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e
const pluginPrefix = "odo"
// NewCmdOdo creates a new root command for odo
func NewCmdOdo(name, fullName string) *cobra.Command {
rootCmd := odoRootCmd(name, fullName)
func NewCmdOdo(ctx context.Context, name, fullName string) *cobra.Command {
rootCmd := odoRootCmd(ctx, name, fullName)
if len(os.Args) > 1 {
cmdPathPieces := os.Args[1:]
@@ -120,7 +121,7 @@ func NewCmdOdo(name, fullName string) *cobra.Command {
return rootCmd
}
func odoRootCmd(name, fullName string) *cobra.Command {
func odoRootCmd(ctx context.Context, name, fullName string) *cobra.Command {
// rootCmd represents the base command when called without any subcommands
rootCmd := &cobra.Command{
Use: name,
@@ -135,7 +136,7 @@ func odoRootCmd(name, fullName string) *cobra.Command {
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.odo.yaml)")
commonflags.AddOutputFlag()
commonflags.AddRunOnFlag()
commonflags.AddRunOnFlag(ctx)
commonflags.AddVariablesFlags()
// Here we add the necessary "logging" flags.. However, we choose to hide some of these from the user
@@ -177,7 +178,7 @@ func odoRootCmd(name, fullName string) *cobra.Command {
login.NewCmdLogin(login.RecommendedCommandName, util.GetFullName(fullName, login.RecommendedCommandName)),
logout.NewCmdLogout(logout.RecommendedCommandName, util.GetFullName(fullName, logout.RecommendedCommandName)),
version.NewCmdVersion(version.RecommendedCommandName, util.GetFullName(fullName, version.RecommendedCommandName)),
preference.NewCmdPreference(preference.RecommendedCommandName, util.GetFullName(fullName, preference.RecommendedCommandName)),
preference.NewCmdPreference(ctx, preference.RecommendedCommandName, util.GetFullName(fullName, preference.RecommendedCommandName)),
telemetry.NewCmdTelemetry(telemetry.RecommendedCommandName),
list.NewCmdList(list.RecommendedCommandName, util.GetFullName(fullName, list.RecommendedCommandName)),
build_images.NewCmdBuildImages(build_images.RecommendedCommandName, util.GetFullName(fullName, build_images.RecommendedCommandName)),

View File

@@ -3,7 +3,6 @@ package deploy
import (
"context"
"fmt"
"path/filepath"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/log"
@@ -68,10 +67,7 @@ func (o *DeployOptions) Validate(ctx context.Context) error {
func (o *DeployOptions) Run(ctx context.Context) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
devfileName = odocontext.GetComponentName(ctx)
appName = odocontext.GetApplication(ctx)
namespace = odocontext.GetNamespace(ctx)
)
@@ -85,7 +81,7 @@ func (o *DeployOptions) Run(ctx context.Context) error {
"odo version: "+version.VERSION)
// Run actual deploy command to be used
err := o.clientset.DeployClient.Deploy(o.clientset.FS, *devfileObj, path, appName, devfileName)
err := o.clientset.DeployClient.Deploy(ctx)
if err == nil {
log.Info("\nYour Devfile has been successfully deployed")

View File

@@ -143,10 +143,6 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
log.Section("Deploying to the cluster in developer mode")
return o.clientset.DevClient.Start(
o.ctx,
*devFileObj,
componentName,
path,
devfilePath,
o.out,
o.errOut,
dev.StartOptions{

View File

@@ -1,7 +1,9 @@
package feature
import (
"os"
"context"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
)
const (
@@ -9,6 +11,6 @@ const (
OdoExperimentalModeTrue = "true"
)
func isExperimentalModeEnabled() bool {
return os.Getenv(OdoExperimentalModeEnvVar) == OdoExperimentalModeTrue
func isExperimentalModeEnabled(ctx context.Context) bool {
return envcontext.GetEnvConfig(ctx).OdoExperimentalMode
}

View File

@@ -1,6 +1,10 @@
package feature
import "github.com/redhat-developer/odo/pkg/log"
import (
"context"
"github.com/redhat-developer/odo/pkg/log"
)
// OdoFeature represents a uniquely identifiable feature of odo.
// It can either be a CLI command or flag.
@@ -29,14 +33,14 @@ var (
// IsEnabled returns whether the specified feature should be enabled or not.
// If the feature is not marked as experimental, it should always be enabled.
// Otherwise, it is enabled only if the experimental mode is enabled (see the isExperimentalModeEnabled package-level function).
func IsEnabled(feat OdoFeature) bool {
func IsEnabled(ctx context.Context, feat OdoFeature) bool {
// Features not marked as experimental are always enabled, regardless of the experimental mode
if !feat.isExperimental {
return true
}
// Features marked as experimental are enabled only if the experimental mode is set
experimentalModeEnabled := isExperimentalModeEnabled()
experimentalModeEnabled := isExperimentalModeEnabled(ctx)
if experimentalModeEnabled {
log.Experimentalf("Experimental mode enabled for %s. Use at your own risk. More details on https://odo.dev/docs/user-guides/advanced/experimental-mode",
feat.description)

View File

@@ -1,14 +1,19 @@
package feature
import "testing"
import (
"context"
"testing"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
)
func TestIsEnabled(t *testing.T) {
type args struct {
feature OdoFeature
}
type env struct {
hasExperimentalModeEnvVar bool
experimentalMode string
experimentalMode bool
}
type testCase struct {
name string
@@ -37,8 +42,7 @@ func TestIsEnabled(t *testing.T) {
name: "non-experimental feature should always be enabled regardless of experimental mode",
args: args{feature: nonExperimentalFeature},
env: env{
hasExperimentalModeEnvVar: true,
experimentalMode: OdoExperimentalModeTrue,
experimentalMode: true,
},
want: true,
},
@@ -46,8 +50,7 @@ func TestIsEnabled(t *testing.T) {
name: "non-experimental feature should always be enabled even if experimental mode is not enabled",
args: args{feature: nonExperimentalFeature},
env: env{
hasExperimentalModeEnvVar: true,
experimentalMode: "false",
experimentalMode: false,
},
want: true,
},
@@ -60,17 +63,7 @@ func TestIsEnabled(t *testing.T) {
name: "experimental feature should be disabled if experimental mode has an unknown value",
args: args{feature: experimentalFeature},
env: env{
hasExperimentalModeEnvVar: true,
experimentalMode: "false",
},
want: false,
},
{
name: "experimental feature should be disabled if experimental mode has an unknown value",
args: args{feature: experimentalFeature},
env: env{
hasExperimentalModeEnvVar: true,
experimentalMode: "foobar",
experimentalMode: false,
},
want: false,
},
@@ -78,18 +71,18 @@ func TestIsEnabled(t *testing.T) {
name: "experimental feature should be enabled only if experimental mode is enabled",
args: args{feature: experimentalFeature},
env: env{
hasExperimentalModeEnvVar: true,
experimentalMode: OdoExperimentalModeTrue,
experimentalMode: true,
},
want: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.env.hasExperimentalModeEnvVar {
t.Setenv(OdoExperimentalModeEnvVar, tt.env.experimentalMode)
}
ctx := context.Background()
cfg := config.Configuration{}
cfg.OdoExperimentalMode = tt.env.experimentalMode
ctx = envcontext.WithEnvConfig(ctx, cfg)
got := IsEnabled(tt.args.feature)
got := IsEnabled(ctx, tt.args.feature)
if got != tt.want {
t.Errorf("IsEnabled: expected %v, but got %v. Env: %v", tt.want, got, tt.env)

View File

@@ -192,7 +192,7 @@ func (o *InitOptions) run(ctx context.Context) (devfileObj parser.DevfileObj, pa
log.Info(messages.InteractiveModeEnabled)
}
devfileObj, devfilePath, devfileLocation, err := o.clientset.InitClient.SelectAndPersonalizeDevfile(o.flags, workingDir)
devfileObj, devfilePath, devfileLocation, err := o.clientset.InitClient.SelectAndPersonalizeDevfile(ctx, o.flags, workingDir)
if err != nil {
return parser.DevfileObj{}, "", "", nil, nil, err
}

View File

@@ -1,6 +1,7 @@
package preference
import (
"context"
"fmt"
"github.com/redhat-developer/odo/pkg/odo/cli/preference/add"
@@ -20,11 +21,11 @@ var preferenceLongDesc = ktemplates.LongDesc(`Modifies odo specific configuratio
%[1]s`)
// NewCmdPreference implements the utils config odo command
func NewCmdPreference(name, fullName string) *cobra.Command {
func NewCmdPreference(ctx context.Context, name, fullName string) *cobra.Command {
// Main Commands
preferenceViewCmd := NewCmdView(viewCommandName, util.GetFullName(fullName, viewCommandName))
preferenceSetCmd := NewCmdSet(setCommandName, util.GetFullName(fullName, setCommandName))
preferenceSetCmd := NewCmdSet(ctx, setCommandName, util.GetFullName(fullName, setCommandName))
preferenceUnsetCmd := NewCmdUnset(unsetCommandName, util.GetFullName(fullName, unsetCommandName))
preferenceAddCmd := add.NewCmdAdd(add.RecommendedCommandName, util.GetFullName(fullName, add.RecommendedCommandName))
preferenceRemoveCmd := remove.NewCmdRemove(remove.RecommendedCommandName, util.GetFullName(fullName, remove.RecommendedCommandName))

View File

@@ -87,14 +87,14 @@ func (o *SetOptions) Run(ctx context.Context) (err error) {
}
// NewCmdSet implements the config set odo command
func NewCmdSet(name, fullName string) *cobra.Command {
func NewCmdSet(ctx context.Context, name, fullName string) *cobra.Command {
o := NewSetOptions()
preferenceSetCmd := &cobra.Command{
Use: name,
Short: "Set a value in the odo preference file",
Long: fmt.Sprintf(setLongDesc, preference.FormatSupportedParameters()),
Example: func(exampleString, fullName string) string {
prefClient, err := preference.NewClient()
prefClient, err := preference.NewClient(ctx)
if err != nil {
util.LogErrorAndExit(err, "unable to set preference, something is wrong with odo, kindly raise an issue at https://github.com/redhat-developer/odo/issues/new?template=Bug.md")
}

View File

@@ -66,7 +66,7 @@ func (o *ListOptions) SetClientset(clientset *clientset.Clientset) {
// Complete completes ListOptions after they've been created
func (o *ListOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) (err error) {
o.devfileList, err = o.clientset.RegistryClient.ListDevfileStacks(o.registryFlag, o.devfileFlag, o.filterFlag, o.detailsFlag)
o.devfileList, err = o.clientset.RegistryClient.ListDevfileStacks(ctx, o.registryFlag, o.devfileFlag, o.filterFlag, o.detailsFlag)
if err != nil {
return err
}

View File

@@ -4,13 +4,18 @@ import (
"context"
"encoding/json"
"github.com/spf13/cobra"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
"github.com/redhat-developer/odo/pkg/segment"
scontext "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/redhat-developer/odo/pkg/util"
"github.com/spf13/cobra"
"k8s.io/klog"
"k8s.io/utils/pointer"
)
const RecommendedCommandName = "telemetry"
@@ -42,23 +47,23 @@ func (o *TelemetryOptions) Validate(ctx context.Context) (err error) {
}
func (o *TelemetryOptions) Run(ctx context.Context) (err error) {
if !segment.IsTelemetryEnabled(o.clientset.PreferenceClient) {
if !scontext.GetTelemetryStatus(ctx) {
return nil
}
dt := segment.GetDebugTelemetryFile()
dt := pointer.StringDeref(envcontext.GetEnvConfig(ctx).OdoDebugTelemetryFile, "")
if len(dt) > 0 {
klog.V(4).Infof("WARNING: telemetry debug enabled, data logged to file %s", dt)
return util.WriteToJSONFile(o.telemetryData, dt)
}
segmentClient, err := segment.NewClient(o.clientset.PreferenceClient)
segmentClient, err := segment.NewClient()
if err != nil {
klog.V(4).Infof("Cannot create a segment client. Will not send any data: %q", err)
}
defer segmentClient.Close()
err = segmentClient.Upload(o.telemetryData)
err = segmentClient.Upload(ctx, o.telemetryData)
if err != nil {
klog.V(4).Infof("Cannot send data to telemetry: %q", err)
}
@@ -84,6 +89,6 @@ func NewCmdTelemetry(name string) *cobra.Command {
genericclioptions.GenericRun(o, cmd, args)
},
}
clientset.Add(telemetryCmd, clientset.PREFERENCE)
clientset.Add(telemetryCmd)
return telemetryCmd
}

View File

@@ -1,22 +1,28 @@
package commonflags
import (
"context"
"flag"
"os"
"testing"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/spf13/pflag"
"k8s.io/klog"
"github.com/redhat-developer/odo/pkg/odo/cli/feature"
)
func TestMain(m *testing.M) {
// --run-on is considered experimental for now. As such, to exist, it requires the ODO_EXPERIMENTAL_MODE env var to be set.
os.Setenv(feature.OdoExperimentalModeEnvVar, feature.OdoExperimentalModeTrue)
ctx := context.Background()
cfg := config.Configuration{
OdoExperimentalMode: true,
}
ctx = envcontext.WithEnvConfig(ctx, cfg)
klog.InitFlags(nil)
AddOutputFlag()
AddRunOnFlag()
AddRunOnFlag(ctx)
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
os.Exit(m.Run())

View File

@@ -3,8 +3,9 @@ package commonflags
import (
"errors"
"flag"
"os"
"strconv"
"github.com/redhat-developer/odo/pkg/config"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/spf13/cobra"
@@ -35,7 +36,7 @@ func AddOutputFlag() {
// CheckMachineReadableOutputCommand performs machine-readable output functions required to
// have it work correctly
func CheckMachineReadableOutputCommand(cmd *cobra.Command) error {
func CheckMachineReadableOutputCommand(envconfig *config.Configuration, cmd *cobra.Command) error {
// Get the needed values
outputFlag := pflag.Lookup(OutputFlagName)
@@ -71,8 +72,8 @@ func CheckMachineReadableOutputCommand(cmd *cobra.Command) error {
// Override the logging level by the value (if set) by the ODO_LOG_LEVEL env
// The "-v" flag set on command line will take precedence over ODO_LOG_LEVEL env
v := flag.CommandLine.Lookup("v").Value.String()
if level, ok := os.LookupEnv("ODO_LOG_LEVEL"); ok && v == "0" {
_ = flag.CommandLine.Set("v", level)
if envconfig.OdoLogLevel != nil && v == "0" {
_ = flag.CommandLine.Set("v", strconv.Itoa(*envconfig.OdoLogLevel))
}
}
return nil

View File

@@ -14,7 +14,7 @@ func TestUseOutputFlagOK(t *testing.T) {
if err != nil {
t.Errorf("Set error should be nil but is %v", err)
}
err = CheckMachineReadableOutputCommand(cmd)
err = CheckMachineReadableOutputCommand(nil, cmd)
if err != nil {
t.Errorf("Check error should be nil but is %v", err)
}
@@ -26,7 +26,7 @@ func TestUseOutputFlagNotUsed(t *testing.T) {
if err != nil {
t.Errorf("Set error should be nil but is %v", err)
}
err = CheckMachineReadableOutputCommand(cmd)
err = CheckMachineReadableOutputCommand(nil, cmd)
if err.Error() != "Machine readable output is not yet implemented for this command" {
t.Errorf("Check error is %v", err)
}
@@ -38,7 +38,7 @@ func TestUseOutputFlagWrongValue(t *testing.T) {
if err != nil {
t.Errorf("Set error should be nil but is %v", err)
}
err = CheckMachineReadableOutputCommand(cmd)
err = CheckMachineReadableOutputCommand(nil, cmd)
if err.Error() != "Please input a valid output format for -o, available format: json" {
t.Errorf("Check error is %v", err)
}

View File

@@ -1,6 +1,7 @@
package commonflags
import (
"context"
"errors"
"flag"
"fmt"
@@ -32,8 +33,8 @@ func UseRunOnFlag(cmd *cobra.Command) {
// We use "flag" in order to make this accessible throughtout ALL of odo, rather than the
// above traditional "persistentflags" usage that does not make it a pointer within the 'pflag'
// package
func AddRunOnFlag() {
if feature.IsEnabled(feature.GenericRunOnFlag) {
func AddRunOnFlag(ctx context.Context) {
if feature.IsEnabled(ctx, feature.GenericRunOnFlag) {
flag.CommandLine.String(RunOnFlagName, "", `Specify target platform, supported platforms: "cluster" (default), "podman" (experimental)`)
_ = pflag.CommandLine.MarkHidden(RunOnFlagName)
}

View File

@@ -83,7 +83,7 @@ const (
var subdeps map[string][]string = map[string][]string{
ALIZER: {REGISTRY},
DELETE_COMPONENT: {KUBERNETES, EXEC},
DEPLOY: {KUBERNETES},
DEPLOY: {KUBERNETES, FILESYSTEM},
DEV: {BINDING, EXEC, FILESYSTEM, KUBERNETES, PORT_FORWARD, PREFERENCE, SYNC, WATCH},
EXEC: {KUBERNETES},
INIT: {ALIZER, FILESYSTEM, PREFERENCE, REGISTRY},
@@ -153,7 +153,7 @@ func Fetch(command *cobra.Command, platform string) (*Clientset, error) {
}
}
if isDefined(command, PREFERENCE) {
dep.PreferenceClient, err = preference.NewClient()
dep.PreferenceClient, err = preference.NewClient(command.Context())
if err != nil {
return nil, err
}
@@ -178,7 +178,7 @@ func Fetch(command *cobra.Command, platform string) (*Clientset, error) {
dep.DeleteClient = _delete.NewDeleteComponentClient(dep.KubernetesClient, dep.ExecClient)
}
if isDefined(command, DEPLOY) {
dep.DeployClient = deploy.NewDeployClient(dep.KubernetesClient)
dep.DeployClient = deploy.NewDeployClient(dep.KubernetesClient, dep.FS)
}
if isDefined(command, INIT) {
dep.InitClient = _init.NewInitClient(dep.FS, dep.PreferenceClient, dep.RegistryClient, dep.AlizerClient)

View File

@@ -1,6 +1,8 @@
package genericclioptions
import (
"context"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/log"
@@ -12,7 +14,7 @@ import (
)
// runPreInit executes the Init command before running the main command
func runPreInit(workingDir string, deps *clientset.Clientset, cmdline cmdline.Cmdline, msg string) error {
func runPreInit(ctx context.Context, workingDir string, deps *clientset.Clientset, cmdline cmdline.Cmdline, msg string) error {
isEmptyDir, err := location.DirIsEmpty(deps.FS, workingDir)
if err != nil {
return err
@@ -23,7 +25,7 @@ func runPreInit(workingDir string, deps *clientset.Clientset, cmdline cmdline.Cm
initFlags := deps.InitClient.GetFlags(cmdline.GetFlags())
err = deps.InitClient.InitDevfile(initFlags, workingDir,
err = deps.InitClient.InitDevfile(ctx, initFlags, workingDir,
func(interactiveMode bool) {
scontext.SetInteractive(cmdline.Context(), interactiveMode)
if interactiveMode {

View File

@@ -29,7 +29,9 @@ import (
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
"k8s.io/klog"
"k8s.io/utils/pointer"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
fcontext "github.com/redhat-developer/odo/pkg/odo/commonflags/context"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/preference"
@@ -78,14 +80,23 @@ const (
)
func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
var err error
startTime := time.Now()
cfg, _ := preference.NewClient()
var (
err error
startTime = time.Now()
ctx = cmd.Context()
)
userConfig, _ := preference.NewClient(ctx)
envConfig := envcontext.GetEnvConfig(ctx)
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still need to support it
disableTelemetryValue, disableTelemetryEnvSet := os.LookupEnv(segment.DisableTelemetryEnv)
disableTelemetry, _ := strconv.ParseBool(disableTelemetryValue)
debugTelemetry := segment.GetDebugTelemetryFile()
isTrackingConsentEnabled, trackingConsentEnvSet, trackingConsentErr := segment.IsTrackingConsentEnabled()
disableTelemetryEnvSet := envConfig.OdoDisableTelemetry != nil
var disableTelemetry bool
if disableTelemetryEnvSet {
disableTelemetry = *envConfig.OdoDisableTelemetry
}
debugTelemetry := pointer.StringDeref(envConfig.OdoDebugTelemetryFile, "")
trackingConsentValue, isTrackingConsentEnabled, trackingConsentEnvSet, trackingConsentErr := segment.IsTrackingConsentEnabled(&envConfig)
// check for conflicting settings
if trackingConsentErr == nil && disableTelemetryEnvSet && trackingConsentEnvSet && disableTelemetry == isTrackingConsentEnabled {
@@ -98,7 +109,7 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
// Prompt the user to consent for telemetry if a value is not set already
// Skip prompting if the preference command is called
// This prompt has been placed here so that it does not prompt the user when they call --help
if !cfg.IsSet(preference.ConsentTelemetrySetting) && cmd.Parent().Name() != "preference" {
if !userConfig.IsSet(preference.ConsentTelemetrySetting) && cmd.Parent().Name() != "preference" {
if !segment.RunningInTerminal() {
klog.V(4).Infof("Skipping telemetry question because there is no terminal (tty)\n")
} else {
@@ -107,15 +118,14 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
klog.V(4).Infof("error in determining value of tracking consent env var: %v", trackingConsentErr)
askConsent = true
} else if trackingConsentEnvSet {
trackingConsent := os.Getenv(segment.TrackingConsentEnv)
if isTrackingConsentEnabled {
klog.V(4).Infof("Skipping telemetry question due to %s=%s\n", segment.TrackingConsentEnv, trackingConsent)
klog.V(4).Infof("Skipping telemetry question due to %s=%s\n", segment.TrackingConsentEnv, trackingConsentValue)
klog.V(4).Info("Telemetry is enabled!\n")
if err1 := cfg.SetConfiguration(preference.ConsentTelemetrySetting, "true"); err1 != nil {
if err1 := userConfig.SetConfiguration(preference.ConsentTelemetrySetting, "true"); err1 != nil {
klog.V(4).Info(err1.Error())
}
} else {
klog.V(4).Infof("Skipping telemetry question due to %s=%s\n", segment.TrackingConsentEnv, trackingConsent)
klog.V(4).Infof("Skipping telemetry question due to %s=%s\n", segment.TrackingConsentEnv, trackingConsentValue)
}
} else if disableTelemetry {
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still need to support it
@@ -129,7 +139,7 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
err = survey.AskOne(prompt, &consentTelemetry, nil)
ui.HandleError(err)
if err == nil {
if err1 := cfg.SetConfiguration(preference.ConsentTelemetrySetting, strconv.FormatBool(consentTelemetry)); err1 != nil {
if err1 := userConfig.SetConfiguration(preference.ConsentTelemetrySetting, strconv.FormatBool(consentTelemetry)); err1 != nil {
klog.V(4).Info(err1.Error())
}
}
@@ -140,14 +150,15 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
klog.V(4).Infof("WARNING: debug telemetry, if enabled, will be logged in %s", debugTelemetry)
}
err = scontext.SetCaller(cmd.Context(), os.Getenv(segment.TelemetryCaller))
// We can dereference as there is a default value defined for this config field
err = scontext.SetCaller(cmd.Context(), envConfig.TelemetryCaller)
if err != nil {
klog.V(3).Infof("error handling caller property for telemetry: %v", err)
}
scontext.SetFlags(cmd.Context(), cmd.Flags())
// set value for telemetry status in context so that we do not need to call IsTelemetryEnabled every time to check its status
scontext.SetTelemetryStatus(cmd.Context(), segment.IsTelemetryEnabled(cfg))
scontext.SetTelemetryStatus(cmd.Context(), segment.IsTelemetryEnabled(userConfig, envConfig))
// Send data to telemetry in case of user interrupt
captureSignals := []os.Signal{syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, os.Interrupt}
@@ -163,7 +174,7 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
startTelemetry(cmd, err, startTime)
})
util.LogErrorAndExit(commonflags.CheckMachineReadableOutputCommand(cmd), "")
util.LogErrorAndExit(commonflags.CheckMachineReadableOutputCommand(&envConfig, cmd), "")
util.LogErrorAndExit(commonflags.CheckRunOnCommand(cmd), "")
util.LogErrorAndExit(commonflags.CheckVariablesCommand(cmd), "")
@@ -175,7 +186,6 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
}
o.SetClientset(deps)
ctx := cmdLineObj.Context()
ctx = fcontext.WithJsonOutput(ctx, commonflags.GetJsonOutputValue(cmdLineObj))
ctx = fcontext.WithRunOn(ctx, platform)
ctx = odocontext.WithApplication(ctx, defaultAppName)
@@ -201,7 +211,7 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) {
if preiniter, ok := o.(PreIniter); ok {
msg := preiniter.PreInit()
err = runPreInit(cwd, deps, cmdLineObj, msg)
err = runPreInit(ctx, cwd, deps, cmdLineObj, msg)
if err != nil {
startTelemetry(cmd, err, startTime)
}

View File

@@ -1,6 +1,7 @@
package preference
import (
"context"
"fmt"
"os"
"os/user"
@@ -9,6 +10,7 @@ import (
"strings"
"time"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
"github.com/redhat-developer/odo/pkg/util"
@@ -69,9 +71,10 @@ type preferenceInfo struct {
var _ Client = (*preferenceInfo)(nil)
func getPreferenceFile() (string, error) {
if env, ok := os.LookupEnv(GlobalConfigEnvName); ok {
return env, nil
func getPreferenceFile(ctx context.Context) (string, error) {
envConfig := envcontext.GetEnvConfig(ctx)
if envConfig.Globalodoconfig != nil {
return *envConfig.Globalodoconfig, nil
}
if len(customHomeDir) != 0 {
@@ -85,8 +88,8 @@ func getPreferenceFile() (string, error) {
return filepath.Join(currentUser.HomeDir, ".odo", configFileName), nil
}
func NewClient() (Client, error) {
return newPreferenceInfo()
func NewClient(ctx context.Context) (Client, error) {
return newPreferenceInfo(ctx)
}
// newPreference creates an empty Preference struct with type meta information
@@ -101,8 +104,8 @@ func newPreference() Preference {
// newPreferenceInfo gets the PreferenceInfo from preference file
// or returns default PreferenceInfo if preference file does not exist
func newPreferenceInfo() (*preferenceInfo, error) {
preferenceFile, err := getPreferenceFile()
func newPreferenceInfo(ctx context.Context) (*preferenceInfo, error) {
preferenceFile, err := getPreferenceFile(ctx)
klog.V(4).Infof("The path for preference file is %+v", preferenceFile)
if err != nil {
return nil, err

View File

@@ -1,6 +1,7 @@
package preference
import (
"context"
"fmt"
"io/ioutil"
"os"
@@ -9,6 +10,9 @@ import (
"testing"
"time"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -19,7 +23,7 @@ func TestNew(t *testing.T) {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
tempConfigFileName := tempConfigFile.Name()
tests := []struct {
name string
@@ -52,7 +56,11 @@ func TestNew(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfi, err := newPreferenceInfo()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{
Globalodoconfig: &tempConfigFileName,
})
cfi, err := newPreferenceInfo(ctx)
switch test.success {
case true:
if err != nil {
@@ -72,12 +80,6 @@ func TestNew(t *testing.T) {
}
func TestGetPushTimeout(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
nonzeroValue := 5 * time.Second
@@ -103,7 +105,9 @@ func TestGetPushTimeout(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := newPreferenceInfo()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
cfg, err := newPreferenceInfo(ctx)
if err != nil {
t.Error(err)
}
@@ -118,12 +122,6 @@ func TestGetPushTimeout(t *testing.T) {
}
func TestGetTimeout(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
zeroValue := 0 * time.Second
nonzeroValue := 5 * time.Second
tests := []struct {
@@ -159,7 +157,9 @@ func TestGetTimeout(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := newPreferenceInfo()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
cfg, err := newPreferenceInfo(ctx)
if err != nil {
t.Error(err)
}
@@ -174,12 +174,6 @@ func TestGetTimeout(t *testing.T) {
}
func TestSetConfiguration(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
trueValue := true
falseValue := false
minValue := minimumDurationValue
@@ -357,7 +351,9 @@ func TestSetConfiguration(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := newPreferenceInfo()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
cfg, err := newPreferenceInfo(ctx)
if err != nil {
t.Error(err)
}
@@ -402,12 +398,6 @@ func TestSetConfiguration(t *testing.T) {
}
func TestConsentTelemetry(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
trueValue := true
falseValue := false
@@ -457,13 +447,6 @@ func TestConsentTelemetry(t *testing.T) {
}
func TestGetupdateNotification(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
trueValue := true
falseValue := false
@@ -562,13 +545,15 @@ func TestIsSupportedParameter(t *testing.T) {
func TestPreferenceIsntCreatedWhenOdoIsUsed(t *testing.T) {
// cleaning up old odo files if any
filename, err := getPreferenceFile()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
filename, err := getPreferenceFile(ctx)
if err != nil {
t.Error(err)
}
os.RemoveAll(filename)
conf, err := newPreferenceInfo()
conf, err := newPreferenceInfo(ctx)
if err != nil {
t.Errorf("error while creating global preference %v", err)
}
@@ -578,7 +563,9 @@ func TestPreferenceIsntCreatedWhenOdoIsUsed(t *testing.T) {
}
func TestMetaTypePopulatedInPreference(t *testing.T) {
pi, err := newPreferenceInfo()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
pi, err := newPreferenceInfo(ctx)
if err != nil {
t.Error(err)
@@ -687,12 +674,6 @@ func TestHandleWithRegistryExist(t *testing.T) {
}
func TestGetConsentTelemetry(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
trueValue := true
falseValue := false

View File

@@ -2,6 +2,8 @@
package registry
import (
"context"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
dfutil "github.com/devfile/library/pkg/util"
"github.com/devfile/registry-support/registry-library/library"
@@ -13,5 +15,5 @@ type Client interface {
DownloadFileInMemory(params dfutil.HTTPRequestParams) ([]byte, error)
DownloadStarterProject(starterProject *devfilev1.StarterProject, decryptedToken string, contextDir string, verbose bool) error
GetDevfileRegistries(registryName string) ([]api.Registry, error)
ListDevfileStacks(registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error)
ListDevfileStacks(ctx context.Context, registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error)
}

View File

@@ -5,6 +5,7 @@
package registry
import (
context "context"
reflect "reflect"
v1alpha2 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
@@ -82,18 +83,18 @@ func (mr *MockClientMockRecorder) GetDevfileRegistries(registryName interface{})
}
// ListDevfileStacks mocks base method.
func (m *MockClient) ListDevfileStacks(registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error) {
func (m *MockClient) ListDevfileStacks(ctx context.Context, registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListDevfileStacks", registryName, devfileFlag, filterFlag, detailsFlag)
ret := m.ctrl.Call(m, "ListDevfileStacks", ctx, registryName, devfileFlag, filterFlag, detailsFlag)
ret0, _ := ret[0].(DevfileStackList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListDevfileStacks indicates an expected call of ListDevfileStacks.
func (mr *MockClientMockRecorder) ListDevfileStacks(registryName, devfileFlag, filterFlag, detailsFlag interface{}) *gomock.Call {
func (mr *MockClientMockRecorder) ListDevfileStacks(ctx, registryName, devfileFlag, filterFlag, detailsFlag interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDevfileStacks", reflect.TypeOf((*MockClient)(nil).ListDevfileStacks), registryName, devfileFlag, filterFlag, detailsFlag)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDevfileStacks", reflect.TypeOf((*MockClient)(nil).ListDevfileStacks), ctx, registryName, devfileFlag, filterFlag, detailsFlag)
}
// PullStackFromRegistry mocks base method.

View File

@@ -1,6 +1,7 @@
package registry
import (
"context"
"io/ioutil"
"os"
"path"
@@ -89,7 +90,7 @@ func (o RegistryClient) GetDevfileRegistries(registryName string) ([]api.Registr
}
// ListDevfileStacks lists all the available devfile stacks in devfile registry
func (o RegistryClient) ListDevfileStacks(registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error) {
func (o RegistryClient) ListDevfileStacks(ctx context.Context, registryName, devfileFlag, filterFlag string, detailsFlag bool) (DevfileStackList, error) {
catalogDevfileList := &DevfileStackList{}
var err error
@@ -115,7 +116,7 @@ func (o RegistryClient) ListDevfileStacks(registryName, devfileFlag, filterFlag
registry := reg // Needed to prevent the lambda from capturing the value
registryPriority := regPriority // Needed to prevent the lambda from capturing the value
retrieveRegistryIndices.Add(util.ConcurrentTask{ToRun: func(errChannel chan error) {
registryDevfiles, err := getRegistryStacks(o.preferenceClient, registry)
registryDevfiles, err := getRegistryStacks(ctx, o.preferenceClient, registry)
if err != nil {
log.Warningf("Registry %s is not set up properly with error: %v, please check the registry URL, and credential and remove add the registry again (refer to `odo preference add registry --help`)\n", registry.Name, err)
return
@@ -158,7 +159,7 @@ func (o RegistryClient) ListDevfileStacks(registryName, devfileFlag, filterFlag
}
if detailsFlag {
devfileData, err := o.retrieveDevfileDataFromRegistry(devfile.Registry.Name, devfile.Name)
devfileData, err := o.retrieveDevfileDataFromRegistry(ctx, devfile.Registry.Name, devfile.Name)
if err != nil {
return *catalogDevfileList, err
}
@@ -185,7 +186,7 @@ func (o RegistryClient) ListDevfileStacks(registryName, devfileFlag, filterFlag
}
// getRegistryStacks retrieves the registry's index devfile stack entries
func getRegistryStacks(preferenceClient preference.Client, registry api.Registry) ([]api.DevfileStack, error) {
func getRegistryStacks(ctx context.Context, preferenceClient preference.Client, registry api.Registry) ([]api.DevfileStack, error) {
isGithubregistry, err := IsGithubBasedRegistry(registry.URL)
if err != nil {
return nil, err
@@ -194,7 +195,7 @@ func getRegistryStacks(preferenceClient preference.Client, registry api.Registry
return nil, &ErrGithubRegistryNotSupported{}
}
// OCI-based registry
devfileIndex, err := library.GetRegistryIndex(registry.URL, segment.GetRegistryOptions(), indexSchema.StackDevfileType)
devfileIndex, err := library.GetRegistryIndex(registry.URL, segment.GetRegistryOptions(ctx), indexSchema.StackDevfileType)
if err != nil {
return nil, err
}
@@ -221,7 +222,7 @@ func createRegistryDevfiles(registry api.Registry, devfileIndex []indexSchema.Sc
return registryDevfiles, nil
}
func (o RegistryClient) retrieveDevfileDataFromRegistry(registryName string, devfileName string) (api.DevfileData, error) {
func (o RegistryClient) retrieveDevfileDataFromRegistry(ctx context.Context, registryName string, devfileName string) (api.DevfileData, error) {
// Create random temporary file
tmpFile, err := ioutil.TempDir("", "odo")
@@ -241,7 +242,7 @@ func (o RegistryClient) retrieveDevfileDataFromRegistry(registryName string, dev
// 4. We need to read the file from the temporary file, unmarshal it and then return the devfile data
for _, reg = range registries {
if reg.Name == registryName {
err = o.PullStackFromRegistry(reg.URL, devfileName, tmpFile, segment.GetRegistryOptions())
err = o.PullStackFromRegistry(reg.URL, devfileName, tmpFile, segment.GetRegistryOptions(ctx))
if err != nil {
return api.DevfileData{}, err
}

View File

@@ -1,6 +1,7 @@
package registry
import (
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -10,7 +11,10 @@ import (
"github.com/golang/mock/gomock"
"github.com/kylelemons/godebug/pretty"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
@@ -35,8 +39,7 @@ OdoSettings:
if err != nil {
t.Error(err)
}
t.Setenv(preference.GlobalConfigEnvName, tempConfigFile.Name())
tempConfigFileName := tempConfigFile.Name()
tests := []struct {
name string
@@ -74,7 +77,11 @@ OdoSettings:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefClient, _ := preference.NewClient()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{
Globalodoconfig: &tempConfigFileName,
})
prefClient, _ := preference.NewClient(ctx)
catClient := NewRegistryClient(filesystem.NewFakeFs(), prefClient)
got, err := catClient.GetDevfileRegistries(tt.registryName)
if err != nil {
@@ -260,7 +267,9 @@ func TestListDevfileStacks(t *testing.T) {
},
}).AnyTimes()
catClient := NewRegistryClient(filesystem.NewFakeFs(), prefClient)
got, err := catClient.ListDevfileStacks(tt.registryName, tt.devfileName, tt.filter, false)
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
got, err := catClient.ListDevfileStacks(ctx, tt.registryName, tt.devfileName, tt.filter, false)
if err != nil {
t.Error(err)
}
@@ -336,7 +345,9 @@ func TestGetRegistryDevfiles(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
prefClient := preference.NewMockClient(ctrl)
got, err := getRegistryStacks(prefClient, tt.registry)
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{})
got, err := getRegistryStacks(ctx, prefClient, tt.registry)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Got: %v, want: %v", got, tt.want)

View File

@@ -1,25 +1,23 @@
package registry
import (
"context"
"io/ioutil"
"reflect"
"testing"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
)
const (
// GlobalConfigEnvName is the environment variable GLOBALODOCONFIG
GlobalConfigEnvName = "GLOBALODOCONFIG"
)
func TestIsSecure(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal(err)
}
defer tempConfigFile.Close()
t.Setenv(GlobalConfigEnvName, tempConfigFile.Name())
tempConfigFileName := tempConfigFile.Name()
tests := []struct {
name string
@@ -52,7 +50,11 @@ func TestIsSecure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := preference.NewClient()
ctx := context.Background()
ctx = envcontext.WithEnvConfig(ctx, config.Configuration{
Globalodoconfig: &tempConfigFileName,
})
cfg, err := preference.NewClient(ctx)
if err != nil {
t.Errorf("Unable to get preference file with error: %v", err)
}

View File

@@ -1,33 +1,32 @@
package segment
import (
"github.com/Xuanwo/go-locale"
registryLibrary "github.com/devfile/registry-support/registry-library/library"
"k8s.io/klog"
"context"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/Xuanwo/go-locale"
registryLibrary "github.com/devfile/registry-support/registry-library/library"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
scontext "github.com/redhat-developer/odo/pkg/segment/context"
"k8s.io/klog"
)
// getTelemetryForDevfileRegistry returns a populated TelemetryData object that contains some odo telemetry (with client consent), such as the anonymous ID and
// locale in addition to the generic client name "odo"
func getTelemetryForDevfileRegistry() (registryLibrary.TelemetryData, error) {
func getTelemetryForDevfileRegistry(ctx context.Context) (registryLibrary.TelemetryData, error) {
td := registryLibrary.TelemetryData{
Client: TelemetryClient,
}
if GetDebugTelemetryFile() != "" {
envConfig := envcontext.GetEnvConfig(ctx)
if envConfig.OdoDebugTelemetryFile != nil {
return td, nil
}
// TODO(feloy) Get from DI
cfg, err := preference.NewClient()
if err != nil {
return td, err
}
if !IsTelemetryEnabled(cfg) {
if !scontext.GetTelemetryStatus(ctx) {
return td, nil
}
@@ -48,8 +47,8 @@ func getTelemetryForDevfileRegistry() (registryLibrary.TelemetryData, error) {
}
// GetRegistryOptions returns a populated RegistryOptions object containing all the properties needed to make a devfile registry library call
func GetRegistryOptions() registryLibrary.RegistryOptions {
td, err := getTelemetryForDevfileRegistry()
func GetRegistryOptions(ctx context.Context) registryLibrary.RegistryOptions {
td, err := getTelemetryForDevfileRegistry(ctx)
if err != nil {
// this error should not prevent basic telemetry from being sent
klog.Errorf("An error prevented additional telemetry to be set %v", err)

View File

@@ -1,13 +1,19 @@
package segment
import (
"context"
"errors"
"fmt"
"io/ioutil"
"testing"
"github.com/devfile/registry-support/registry-library/library"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
scontext "github.com/redhat-developer/odo/pkg/segment/context"
)
func TestGetRegistryOptions(t *testing.T) {
@@ -21,50 +27,45 @@ func TestGetRegistryOptions(t *testing.T) {
tests := []struct {
testName string
consent string
consent bool
telemetryFile bool
cfg preference.Client
}{
{
testName: "Registry options with telemetry consent and telemetry file",
consent: "true",
consent: true,
telemetryFile: true,
},
{
testName: "Registry options with telemetry consent and no telemetry file",
consent: "true",
consent: true,
telemetryFile: false,
},
{
testName: "Registry options without telemetry consent and telemetry file",
consent: "false",
consent: false,
telemetryFile: true,
},
{
testName: "Registry options without telemetry consent and no telemetry file",
consent: "false",
consent: false,
telemetryFile: false,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
cfg, err := preference.NewClient()
if err != nil {
t.Error(err)
}
err = cfg.SetConfiguration(preference.ConsentTelemetrySetting, tt.consent)
if err != nil {
t.Error(err)
}
ctx := scontext.NewContext(context.Background())
var envConfig config.Configuration
if tt.telemetryFile {
t.Setenv(DebugTelemetryFileEnv, "/a/telemetry/file")
envConfig.OdoDebugTelemetryFile = pointer.String("/a/telemetry/file")
}
ctx = envcontext.WithEnvConfig(ctx, envConfig)
scontext.SetTelemetryStatus(ctx, tt.consent)
ro := GetRegistryOptions()
err = verifyRegistryOptions(cfg.GetConsentTelemetry(), tt.telemetryFile, ro)
ro := GetRegistryOptions(ctx)
err = verifyRegistryOptions(tt.consent, tt.telemetryFile, ro)
if err != nil {
t.Error(err)
}

View File

@@ -1,6 +1,7 @@
package segment
import (
"context"
"errors"
"fmt"
"io/ioutil"
@@ -10,18 +11,19 @@ import (
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/Xuanwo/go-locale"
"github.com/redhat-developer/odo/pkg/config"
scontext "github.com/redhat-developer/odo/pkg/segment/context"
"github.com/pborman/uuid"
"golang.org/x/term"
"gopkg.in/segmentio/analytics-go.v3"
"k8s.io/klog"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/preference"
)
@@ -47,9 +49,7 @@ const (
// Setting it to 'no' has the same effect as DisableTelemetryEnv=true (telemetry is disabled and no question asked)
// Settings this to 'yes' skips the question about telemetry and enables user tracking.
// Possible values are yes/no.
TrackingConsentEnv = "ODO_TRACKING_CONSENT"
DebugTelemetryFileEnv = "ODO_DEBUG_TELEMETRY_FILE"
TelemetryCaller = "TELEMETRY_CALLER"
TrackingConsentEnv = "ODO_TRACKING_CONSENT"
)
type TelemetryProperties struct {
@@ -70,22 +70,20 @@ type TelemetryData struct {
type Client struct {
// SegmentClient helps interact with the segment API
SegmentClient analytics.Client
// Preference points to the global odo config
Preference preference.Client
// TelemetryFilePath points to the file containing anonymousID used for tracking odo commands executed by the user
TelemetryFilePath string
}
// NewClient returns a Client created with the default args
func NewClient(preference preference.Client) (*Client, error) {
return newCustomClient(preference,
func NewClient() (*Client, error) {
return newCustomClient(
GetTelemetryFilePath(),
analytics.DefaultEndpoint,
)
}
// newCustomClient returns a Client created with custom args
func newCustomClient(preference preference.Client, telemetryFilePath string, segmentEndpoint string) (*Client, error) {
func newCustomClient(telemetryFilePath string, segmentEndpoint string) (*Client, error) {
// get the locale information
tag, err := locale.Detect()
if err != nil {
@@ -109,7 +107,6 @@ func newCustomClient(preference preference.Client, telemetryFilePath string, seg
}
return &Client{
SegmentClient: client,
Preference: preference,
TelemetryFilePath: telemetryFilePath,
}, nil
}
@@ -128,9 +125,9 @@ func (c *Client) Close() error {
}
// Upload prepares the data to be sent to segment and send it once the client connection closes
func (c *Client) Upload(data TelemetryData) error {
func (c *Client) Upload(ctx context.Context, data TelemetryData) error {
// if the user has not consented for telemetry, return
if !IsTelemetryEnabled(c.Preference) {
if !scontext.GetTelemetryStatus(ctx) {
return nil
}
@@ -264,20 +261,20 @@ func RunningInTerminal() bool {
}
// IsTelemetryEnabled returns true if user has consented to telemetry
func IsTelemetryEnabled(cfg preference.Client) bool {
func IsTelemetryEnabled(cfg preference.Client, envConfig config.Configuration) bool {
klog.V(4).Info("Checking telemetry enable status")
// The env variable gets precedence in this decision.
// In case a non-bool value was passed to the env var, we ignore it
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still need to support it
disableTelemetry, _ := strconv.ParseBool(os.Getenv(DisableTelemetryEnv))
disableTelemetry := pointer.BoolDeref(envConfig.OdoDisableTelemetry, false)
if disableTelemetry {
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still need to support it
klog.V(4).Infof("Sending telemetry disabled by %q env variable\n", DisableTelemetryEnv)
return false
}
trackingConsentEnabled, present, err := IsTrackingConsentEnabled()
_, trackingConsentEnabled, present, err := IsTrackingConsentEnabled(&envConfig)
if err != nil {
klog.V(4).Infof("error in determining value of tracking consent env var: %v", err)
} else if present {
@@ -301,18 +298,18 @@ func IsTelemetryEnabled(cfg preference.Client) bool {
// IsTrackingConsentEnabled returns whether tracking consent is enabled, based on the value of the TrackingConsentEnv environment variable.
// The second value returned indicates whether the variable is present in the environment.
func IsTrackingConsentEnabled() (enabled bool, present bool, err error) {
trackingConsent, ok := os.LookupEnv(TrackingConsentEnv)
if !ok {
return false, false, nil
func IsTrackingConsentEnabled(envConfig *config.Configuration) (value string, enabled bool, present bool, err error) {
if envConfig.OdoTrackingConsent == nil {
return "", false, false, nil
}
trackingConsent := *envConfig.OdoTrackingConsent
switch trackingConsent {
case "yes":
return true, true, nil
return trackingConsent, true, true, nil
case "no":
return false, true, nil
return trackingConsent, false, true, nil
default:
return false, true, fmt.Errorf("invalid value for %s: %q", TrackingConsentEnv, trackingConsent)
return trackingConsent, false, true, fmt.Errorf("invalid value for %s: %q", TrackingConsentEnv, trackingConsent)
}
}
@@ -355,7 +352,3 @@ func sanitizeExec(errString string) string {
errString = pattern.ReplaceAllString(errString, fmt.Sprintf("exec command %s", Sanitizer))
return errString
}
func GetDebugTelemetryFile() string {
return os.Getenv(DebugTelemetryFileEnv)
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/config"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
@@ -66,11 +67,7 @@ func TestClientUploadWithoutConsent(t *testing.T) {
defer server.Close()
defer close(body)
ctrl := gomock.NewController(t)
cfg := preference.NewMockClient(ctrl)
cfg.EXPECT().GetConsentTelemetry().Return(false)
c, err := newCustomClient(cfg, createConfigDir(t), server.URL)
c, err := newCustomClient(createConfigDir(t), server.URL)
if err != nil {
t.Error(err)
}
@@ -78,7 +75,9 @@ func TestClientUploadWithoutConsent(t *testing.T) {
testError := errors.New("error occurred")
uploadData := fakeTelemetryData("odo preference view", testError, context.Background())
// run a command, odo preference view
if err = c.Upload(uploadData); err != nil {
ctx := context.Background()
scontext.SetTelemetryStatus(ctx, false)
if err = c.Upload(ctx, uploadData); err != nil {
t.Error(err)
}
@@ -124,17 +123,15 @@ func TestClientUploadWithConsent(t *testing.T) {
for _, tt := range tests {
t.Log("Running test: ", tt.testName)
t.Run(tt.testName, func(t *testing.T) {
ctrl := gomock.NewController(t)
cfg := preference.NewMockClient(ctrl)
cfg.EXPECT().GetConsentTelemetry().Return(true)
c, err := newCustomClient(cfg, createConfigDir(t), server.URL)
c, err := newCustomClient(createConfigDir(t), server.URL)
if err != nil {
t.Error(err)
}
uploadData := fakeTelemetryData("odo init", tt.err, context.Background())
// upload the data to Segment
if err = c.Upload(uploadData); err != nil {
ctx := scontext.NewContext(context.Background())
scontext.SetTelemetryStatus(ctx, true)
if err = c.Upload(ctx, uploadData); err != nil {
t.Error(err)
}
// segment.Client.SegmentClient uploads the data to server when a condition is met or when the connection is closed.
@@ -240,7 +237,7 @@ func TestIsTelemetryEnabled(t *testing.T) {
// When only ODO_DISABLE_TELEMETRY is present in the env, it takes precedence over the ConsentTelemetry preference,
// only if ODO_DISABLE_TELEMETRY=true
for _, odoDisableTelemetry := range []string{"", "true", "false", "foo"} {
for _, odoDisableTelemetry := range []string{"true", "false"} {
for _, consentTelemetry := range []bool{true, false} {
odoDisableTelemetry := odoDisableTelemetry
consentTelemetry := consentTelemetry
@@ -262,7 +259,7 @@ func TestIsTelemetryEnabled(t *testing.T) {
}
}
//Cases where all the environment variables are there.
for _, odoDisableTelemetry := range []string{"", "true", "false", "foo"} {
for _, odoDisableTelemetry := range []string{"true", "false"} {
for _, odoTrackingConsent := range []string{"", "yes", "no", "bar"} {
for _, consentTelemetry := range []bool{true, false} {
odoDisableTelemetry := odoDisableTelemetry
@@ -300,7 +297,11 @@ func TestIsTelemetryEnabled(t *testing.T) {
cfg := preference.NewMockClient(ctrl)
cfg.EXPECT().GetConsentTelemetry().Return(tt.consentTelemetryPref).AnyTimes()
got := IsTelemetryEnabled(cfg)
envConfig, err := config.GetConfiguration()
if err != nil {
t.Errorf("Get configuration fails: %v", err)
}
got := IsTelemetryEnabled(cfg, *envConfig)
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
want := tt.want(tt.env[DisableTelemetryEnv], tt.env[TrackingConsentEnv], tt.consentTelemetryPref)
@@ -318,10 +319,8 @@ func TestClientUploadWithContext(t *testing.T) {
defer server.Close()
defer close(body)
ctrl := gomock.NewController(t)
cfg := preference.NewMockClient(ctrl)
cfg.EXPECT().GetConsentTelemetry().Return(true).AnyTimes()
ctx := scontext.NewContext(context.Background())
scontext.SetTelemetryStatus(ctx, true)
for k, v := range map[string]string{scontext.ComponentType: "nodejs", scontext.ClusterType: ""} {
switch k {
@@ -333,12 +332,12 @@ func TestClientUploadWithContext(t *testing.T) {
scontext.SetClusterType(ctx, fakeClient)
uploadData = fakeTelemetryData("odo set project", nil, ctx)
}
c, err := newCustomClient(cfg, createConfigDir(t), server.URL)
c, err := newCustomClient(createConfigDir(t), server.URL)
if err != nil {
t.Error(err)
}
// upload the data to Segment
if err = c.Upload(uploadData); err != nil {
if err = c.Upload(ctx, uploadData); err != nil {
t.Error(err)
}
if err = c.Close(); err != nil {

View File

@@ -82,7 +82,7 @@ type WatchParameters struct {
// Custom function that can be used to push detected changes to remote pod. For more info about what each of the parameters to this function, please refer, pkg/component/component.go#PushLocal
// WatchHandler func(kclient.ClientInterface, string, string, string, io.Writer, []string, []string, bool, []string, bool) error
// Custom function that can be used to push detected changes to remote devfile pod. For more info about what each of the parameters to this function, please refer, pkg/devfile/adapters/interface.go#PlatformAdapter
DevfileWatchHandler func(adapters.PushParameters, WatchParameters, *ComponentStatus) error
DevfileWatchHandler func(context.Context, adapters.PushParameters, WatchParameters, *ComponentStatus) error
// Parameter whether or not to show build logs
Show bool
// DevfileBuildCmd takes the build command through the command line and overwrites devfile build command
@@ -114,7 +114,7 @@ type evaluateChangesFunc func(events []fsnotify.Event, path string, fileIgnores
// processEventsFunc processes the events received on the watcher. It uses the WatchParameters to trigger watch handler and writes to out
// It returns a Duration after which to recall in case of error
type processEventsFunc func(changedFiles, deletedPaths []string, parameters WatchParameters, out io.Writer, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error)
type processEventsFunc func(ctx context.Context, changedFiles, deletedPaths []string, parameters WatchParameters, out io.Writer, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error)
func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error {
klog.V(4).Infof("starting WatchAndPush, path: %s, component: %s, ignores %s", parameters.Path, parameters.ComponentName, parameters.FileIgnores)
@@ -240,7 +240,7 @@ func (o *WatchClient) eventWatcher(
componentStatus.State = StateSyncOutdated
fmt.Fprintf(out, "Pushing files...\n\n")
retry, err := processEventsHandler(changedFiles, deletedPaths, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, changedFiles, deletedPaths, parameters, out, &componentStatus, expBackoff)
o.forceSync = false
if err != nil {
return err
@@ -278,7 +278,7 @@ func (o *WatchClient) eventWatcher(
}
case <-deployTimer.C:
retry, err := processEventsHandler(nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -294,7 +294,7 @@ func (o *WatchClient) eventWatcher(
case <-devfileTimer.C:
fmt.Fprintf(out, "Updating Component...\n\n")
retry, err := processEventsHandler(nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -306,7 +306,7 @@ func (o *WatchClient) eventWatcher(
}
case <-retryTimer.C:
retry, err := processEventsHandler(nil, nil, parameters, out, &componentStatus, expBackoff)
retry, err := processEventsHandler(ctx, nil, nil, parameters, out, &componentStatus, expBackoff)
if err != nil {
return err
}
@@ -423,6 +423,7 @@ func evaluateFileChanges(events []fsnotify.Event, path string, fileIgnores []str
}
func (o *WatchClient) processEvents(
ctx context.Context,
changedFiles, deletedPaths []string,
parameters WatchParameters,
out io.Writer,
@@ -451,7 +452,7 @@ func (o *WatchClient) processEvents(
ErrOut: parameters.ErrOut,
}
oldStatus := *componentStatus
err := parameters.DevfileWatchHandler(pushParams, parameters, componentStatus)
err := parameters.DevfileWatchHandler(ctx, pushParams, parameters, componentStatus)
if err != nil {
if isFatal(err) {
return nil, err

View File

@@ -40,7 +40,7 @@ func evaluateChangesHandler(events []fsnotify.Event, path string, fileIgnores []
return changedFiles, deletedPaths
}
func processEventsHandler(changedFiles, deletedPaths []string, _ WatchParameters, out io.Writer, componentStatus *ComponentStatus, backo *ExpBackoff) (*time.Duration, error) {
func processEventsHandler(ctx context.Context, changedFiles, deletedPaths []string, _ WatchParameters, out io.Writer, componentStatus *ComponentStatus, backo *ExpBackoff) (*time.Duration, error) {
fmt.Fprintf(out, "changedFiles %s deletedPaths %s\n", changedFiles, deletedPaths)
return nil, nil
}

View File

@@ -2,6 +2,7 @@ package helper
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
@@ -15,6 +16,8 @@ import (
"github.com/tidwall/gjson"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/segment"
@@ -200,11 +203,21 @@ func CommonBeforeEach(setupCluster bool) CommonVar {
commonVar.Project = commonVar.CliRunner.CreateAndSetRandNamespaceProject()
}
commonVar.OriginalWorkingDirectory = Getwd()
os.Setenv("GLOBALODOCONFIG", filepath.Join(commonVar.ConfigDir, "preference.yaml"))
// Set ConsentTelemetry to false so that it does not prompt to set a preference value
cfg, _ := preference.NewClient()
err := cfg.SetConfiguration(preference.ConsentTelemetrySetting, "false")
configPath := filepath.Join(commonVar.ConfigDir, "preference.yaml")
os.Setenv("GLOBALODOCONFIG", configPath)
// Create context with env var configuration
ctx := context.Background()
envConfig, err := config.GetConfiguration()
Expect(err).To(BeNil())
ctx = envcontext.WithEnvConfig(ctx, *envConfig)
// Set ConsentTelemetry to false so that it does not prompt to set a preference value
cfg, _ := preference.NewClient(ctx)
err = cfg.SetConfiguration(preference.ConsentTelemetrySetting, "false")
Expect(err).To(BeNil())
// Use ephemeral volumes (emptyDir) in tests to make test faster
err = cfg.SetConfiguration(preference.EphemeralSetting, "true")
Expect(err).To(BeNil())

View File

@@ -13,14 +13,17 @@ import (
"github.com/onsi/gomega/types"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/segment"
)
const (
TelemetryCaller = "TELEMETRY_CALLER"
)
func runningCmd(cmd *exec.Cmd) string {
prog := filepath.Base(cmd.Path)
var env []string
for _, e := range cmd.Env {
if strings.HasPrefix(e, "ODO_") || e == segment.TelemetryCaller {
if strings.HasPrefix(e, "ODO_") || e == TelemetryCaller {
env = append(env, e)
}
}

View File

@@ -1,6 +1,7 @@
package helper
import (
"context"
"encoding/json"
"io/ioutil"
"os"
@@ -8,20 +9,32 @@ import (
_ "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/segment"
)
const (
DebugTelemetryFileEnv = "ODO_DEBUG_TELEMETRY_FILE"
)
func setDebugTelemetryFile(value string) error {
return os.Setenv(segment.DebugTelemetryFileEnv, value)
return os.Setenv(DebugTelemetryFileEnv, value)
}
// EnableTelemetryDebug creates a temp file to use for debugging telemetry.
// it also sets up envs and cfg for the same
func EnableTelemetryDebug() {
Expect(os.Setenv(segment.TrackingConsentEnv, "yes")).NotTo(HaveOccurred())
cfg, _ := preference.NewClient()
err := cfg.SetConfiguration(preference.ConsentTelemetrySetting, "true")
ctx := context.Background()
envConfig, err := config.GetConfiguration()
Expect(err).To(BeNil())
ctx = envcontext.WithEnvConfig(ctx, *envConfig)
cfg, _ := preference.NewClient(ctx)
err = cfg.SetConfiguration(preference.ConsentTelemetrySetting, "true")
Expect(err).To(BeNil())
tempFile, err := ioutil.TempFile("", "telemetry")
Expect(err).NotTo(HaveOccurred())
@@ -29,11 +42,15 @@ func EnableTelemetryDebug() {
Expect(tempFile.Close()).NotTo(HaveOccurred())
}
func GetDebugTelemetryFile() string {
return os.Getenv(DebugTelemetryFileEnv)
}
// GetTelemetryDebugData gets telemetry data dumped into temp file for testing/debugging
func GetTelemetryDebugData() segment.TelemetryData {
var data []byte
var td segment.TelemetryData
telemetryFile := segment.GetDebugTelemetryFile()
telemetryFile := GetDebugTelemetryFile()
Eventually(func() string {
d, err := ioutil.ReadFile(telemetryFile)
Expect(err).To(BeNil())
@@ -48,9 +65,15 @@ func GetTelemetryDebugData() segment.TelemetryData {
// ResetTelemetry resets the telemetry back to original values
func ResetTelemetry() {
Expect(os.Setenv(segment.TrackingConsentEnv, "no")).NotTo(HaveOccurred())
Expect(os.Unsetenv(segment.DebugTelemetryFileEnv))
cfg, _ := preference.NewClient()
err := cfg.SetConfiguration(preference.ConsentTelemetrySetting, "true")
Expect(os.Unsetenv(DebugTelemetryFileEnv))
ctx := context.Background()
envConfig, err := config.GetConfiguration()
Expect(err).To(BeNil())
ctx = envcontext.WithEnvConfig(ctx, *envConfig)
cfg, _ := preference.NewClient(ctx)
err = cfg.SetConfiguration(preference.ConsentTelemetrySetting, "true")
Expect(err).NotTo(HaveOccurred())
Expect(segment.IsTelemetryEnabled(cfg)).To(BeFalse())
Expect(segment.IsTelemetryEnabled(cfg, *envConfig)).To(BeFalse())
}

View File

@@ -1,11 +1,14 @@
package integration
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/redhat-developer/odo/pkg/config"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/odo/cli/messages"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/segment"
@@ -378,23 +381,6 @@ var _ = Describe("odo devfile init command tests", Label(helper.LabelNoCluster),
segment.TrackingConsentEnv: "no",
},
},
{
name: "ODO_DISABLE_TELEMETRY=foobar and ODO_TRACKING_CONSENT=no",
env: map[string]string{
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
segment.DisableTelemetryEnv: "foobar-should-evaluate-to-false",
segment.TrackingConsentEnv: "no",
},
},
{
name: "ODO_DISABLE_TELEMETRY='' and ODO_TRACKING_CONSENT=no",
env: map[string]string{
// an empty string will evaluate to false
//lint:ignore SA1019 We deprecated this env var, but until it is removed, we still want to test it
segment.DisableTelemetryEnv: "",
segment.TrackingConsentEnv: "no",
},
},
} {
tt := tt
It("should error out if "+tt.name, func() {
@@ -428,7 +414,7 @@ var _ = Describe("odo devfile init command tests", Label(helper.LabelNoCluster),
{
title: "empty caller env var",
env: map[string]string{
segment.TelemetryCaller: "",
helper.TelemetryCaller: "",
},
callerChecker: func(_, _ string, td segment.TelemetryData) {
cmdProperties := td.Properties.CmdProperties
@@ -439,7 +425,7 @@ var _ = Describe("odo devfile init command tests", Label(helper.LabelNoCluster),
{
title: "invalid caller env var",
env: map[string]string{
segment.TelemetryCaller: "an-invalid-caller",
helper.TelemetryCaller: "an-invalid-caller",
},
callerChecker: func(stdout, stderr string, td segment.TelemetryData) {
By("not disclosing list of allowed values", func() {
@@ -471,7 +457,7 @@ var _ = Describe("odo devfile init command tests", Label(helper.LabelNoCluster),
telemetryTests = append(telemetryTests, telemetryTest{
title: fmt.Sprintf("valid caller env var: %s", c),
env: map[string]string{
segment.TelemetryCaller: c,
helper.TelemetryCaller: c,
},
callerChecker: func(_, _ string, td segment.TelemetryData) {
Expect(td.Properties.CmdProperties[segmentContext.Caller]).To(Equal(c))
@@ -486,7 +472,12 @@ var _ = Describe("odo devfile init command tests", Label(helper.LabelNoCluster),
BeforeEach(func() {
helper.EnableTelemetryDebug()
cfg, err := preference.NewClient()
ctx := context.Background()
envConfig, err := config.GetConfiguration()
Expect(err).To(BeNil())
ctx = envcontext.WithEnvConfig(ctx, *envConfig)
cfg, err := preference.NewClient(ctx)
Expect(err).ShouldNot(HaveOccurred())
if tt.setupFunc != nil {
tt.setupFunc(cfg)

View File

@@ -134,7 +134,7 @@ var _ = Describe("odo generic", func() {
})
Context("experimental mode has an unknown value", func() {
for _, val := range []string{"", "false", "foobar"} {
for _, val := range []string{"", "false"} {
val := val
It("should not list experimental flags if ODO_EXPERIMENTAL is not true", func() {
helpOutput := helper.Cmd("odo", "help").AddEnv(feature.OdoExperimentalModeEnvVar + "=" + val).ShouldPass().Out()

8
vendor/github.com/sethvargo/go-envconfig/AUTHORS generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# This is the list of envconfig authors for copyright purposes.
#
# This does not necessarily list everyone who has contributed code, since in
# some cases, their employer may be the copyright holder. To see the full list
# of contributors, see the revision history in source control.
Google LLC
Seth Vargo

202
vendor/github.com/sethvargo/go-envconfig/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

22
vendor/github.com/sethvargo/go-envconfig/Makefile generated vendored Normal file
View File

@@ -0,0 +1,22 @@
# Copyright The envconfig Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
test:
@go test \
-count=1 \
-race \
-shuffle=on \
-timeout=10m \
./...
.PHONY: test

414
vendor/github.com/sethvargo/go-envconfig/README.md generated vendored Normal file
View File

@@ -0,0 +1,414 @@
# Envconfig
[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/sethvargo/go-envconfig)
[![GitHub Actions](https://img.shields.io/github/workflow/status/sethvargo/go-envconfig/unit/main?style=flat-square)](https://github.com/sethvargo/go-envconfig/actions?query=branch%3Amain+-event%3Aschedule)
Envconfig populates struct field values based on environment variables or
arbitrary lookup functions. It supports pre-setting mutations, which is useful
for things like converting values to uppercase, trimming whitespace, or looking
up secrets.
**Note:** Versions prior to v0.2 used a different import path. This README and
examples are for v0.2+.
## Usage
Define a struct with fields using the `env` tag:
```go
type MyConfig struct {
Port int `env:"PORT"`
Username string `env:"USERNAME"`
}
```
Set some environment variables:
```sh
export PORT=5555
export USERNAME=yoyo
```
Process it using envconfig:
```go
package main
import (
"context"
"log"
"github.com/sethvargo/go-envconfig"
)
func main() {
ctx := context.Background()
var c MyConfig
if err := envconfig.Process(ctx, &c); err != nil {
log.Fatal(err)
}
// c.Port = 5555
// c.Username = "yoyo"
}
```
You can also use nested structs, just remember that any fields you want to
process must be public:
```go
type MyConfig struct {
Database *DatabaseConfig
}
type DatabaseConfig struct {
Port int `env:"PORT"`
Username string `env:"USERNAME"`
}
```
## Configuration
Use the `env` struct tag to define configuration.
### Required
If a field is required, processing will error if the environment variable is
unset.
```go
type MyStruct struct {
Port int `env:"PORT,required"`
}
```
It is invalid to have a field as both `required` and `default`.
### Default
If an environment variable is not set, the field will be set to the default
value. Note that the environment variable must not be set (e.g. `unset PORT`).
If the environment variable is the empty string, that counts as a "value" and
the default will not be used.
```go
type MyStruct struct {
Port int `env:"PORT,default=5555"`
}
```
You can also set the default value to another field or value from the
environment, for example:
```go
type MyStruct struct {
DefaultPort int `env:"DEFAULT_PORT,default=5555"`
Port int `env:"OVERRIDE_PORT,default=$DEFAULT_PORT"`
}
```
The value for `Port` will default to the value of `DEFAULT_PORT`.
It is invalid to have a field as both `required` and `default`.
### Prefix
For shared, embedded structs, you can define a prefix to use when processing
struct values for that embed.
```go
type SharedConfig struct {
Port int `env:"PORT,default=5555"`
}
type Server1 struct {
// This processes Port from $FOO_PORT.
*SharedConfig `env:",prefix=FOO_"`
}
type Server2 struct {
// This processes Port from $BAR_PORT.
*SharedConfig `env:",prefix=BAR_"`
}
```
It is invalid to specify a prefix on non-struct fields.
### Overwrite
If overwrite is set, the value will be overwritten if there is an environment
variable match regardless if the value is non-zero.
```go
type MyStruct struct {
Port int `env:"PORT,overwrite"`
}
```
The rules for overwrite + default are:
- If the struct field has the zero value and a default is set:
- If no environment variable is specified, the struct field will be
populated with the default value.
- If an environment variable is specified, the struct field will be
populate with the environment variable value.
- If the struct field has a non-zero value and a default is set:
- If no environment variable is specified, the struct field's existing
value will be used (the default is ignored).
- If an environment variable is specified, the struct field's existing
value will be overwritten with the environment variable value.
## Complex Types
**Note:** Complex types are only decoded or unmarshalled when the environment
variable is defined or a default is specified. The decoding/unmarshalling
functions are _not_ invoked when a value is not defined.
### Durations
In the environment, `time.Duration` values are specified as a parsable Go
duration:
```go
type MyStruct struct {
MyVar time.Duration `env:"MYVAR"`
}
```
```bash
export MYVAR="10m" # 10 * time.Minute
```
### TextUnmarshaler / BinaryUnmarshaler
Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as such.
### json.Unmarshaler
Types that implement `json.Unmarshaler` are processed as such.
### gob.Decoder
Types that implement `gob.Decoder` are processed as such.
### Slices
Slices are specified as comma-separated values:
```go
type MyStruct struct {
MyVar []string `env:"MYVAR"`
}
```
```bash
export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"}
```
Define a custom delimiter with `delimiter`:
```go
type MyStruct struct {
MyVar []string `env:"MYVAR,delimiter=;"`
```
```bash
export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"}
```
Note that byte slices are special cased and interpreted as strings from the
environment.
### Maps
Maps are specified as comma-separated key:value pairs:
```go
type MyStruct struct {
MyVar map[string]string `env:"MYVAR"`
}
```
```bash
export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"}
```
Define a custom delimiter with `delimiter`:
```go
type MyStruct struct {
MyVar map[string]string `env:"MYVAR,delimiter=;"`
```
```bash
export MYVAR="a:b;c:d" # map[string]string{"a":"b", "c":"d"}
```
Define a separator with `separator`:
```go
type MyStruct struct {
MyVar map[string]string `env:"MYVAR,separator=|"`
}
```
```bash
export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"}
```
### Structs
Envconfig walks the entire struct, including nested structs, so deeply-nested
fields are also supported.
If a nested struct is a pointer type, it will automatically be instantianted to
the non-nil value. To change this behavior, see
[Initialization](#Initialization).
### Custom
You can also [define your own decoder](#Extension).
## Prefixing
You can define a custom prefix using the `PrefixLookuper`. This will lookup
values in the environment by prefixing the keys with the provided value:
```go
type MyStruct struct {
MyVar string `env:"MYVAR"`
}
```
```go
// Process variables, but look for the "APP_" prefix.
l := envconfig.PrefixLookuper("APP_", envconfig.OsLookuper())
if err := envconfig.ProcessWith(ctx, &c, l); err != nil {
panic(err)
}
```
```bash
export APP_MYVAR="foo"
```
## Initialization
By default, all pointer fields are initialized (allocated) so they are not
`nil`. To disable this behavior, use the tag the field as `noinit`:
```go
type MyStruct struct {
// Without `noinit`, DeleteUser would be initialized to the default boolean
// value. With `noinit`, if the environment variable is not given, the value
// is kept as uninitialized (nil).
DeleteUser *bool `env:"DELETE_USER, noinit"`
}
```
This also applies to nested fields in a struct:
```go
type ParentConfig struct {
// Without `noinit` tag, `Child` would be set to `&ChildConfig{}` whether
// or not `FIELD` is set in the env var.
// With `noinit`, `Child` would stay nil if `FIELD` is not set in the env var.
Child *ChildConfig `env:",noinit"`
}
type ChildConfig struct {
Field string `env:"FIELD"`
}
```
The `noinit` tag is only applicable for pointer fields. Putting the tag on a
non-struct-pointer will return an error.
## Extension
All built-in types are supported except `Func` and `Chan`. If you need to define
a custom decoder, implement the `Decoder` interface:
```go
type MyStruct struct {
field string
}
func (v *MyStruct) EnvDecode(val string) error {
v.field = fmt.Sprintf("PREFIX-%s", val)
return nil
}
```
If you need to modify environment variable values before processing, you can
specify a custom `Mutator`:
```go
type Config struct {
Password `env:"PASSWORD"`
}
func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
if strings.HasPrefix(value, "secret://") {
return secretmanager.Resolve(ctx, value) // example
}
return value, nil
}
var config Config
envconfig.ProcessWith(ctx, &config, envconfig.OsLookuper(), resolveSecretFunc)
```
## Testing
Relying on the environment in tests can be troublesome because environment
variables are global, which makes it difficult to parallelize the tests.
Envconfig supports extracting data from anything that returns a value:
```go
lookuper := envconfig.MapLookuper(map[string]string{
"FOO": "bar",
"ZIP": "zap",
})
var config Config
envconfig.ProcessWith(ctx, &config, lookuper)
```
Now you can parallelize all your tests by providing a map for the lookup
function. In fact, that's how the tests in this repo work, so check there for an
example.
You can also combine multiple lookupers with `MultiLookuper`. See the GoDoc for
more information and examples.
## Inspiration
This library is conceptually similar to [kelseyhightower/envconfig](https://github.com/kelseyhightower/envconfig), with the following
major behavioral differences:
- Adds support for specifying a custom lookup function (such as a map), which
is useful for testing.
- Only populates fields if they contain zero or nil values if `overwrite` is
unset. This means you can pre-initialize a struct and any pre-populated
fields will not be overwritten during processing.
- Support for interpolation. The default value for a field can be the value of
another field.
- Support for arbitrary mutators that change/resolve data before type
conversion.

57
vendor/github.com/sethvargo/go-envconfig/decoding.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
// Copyright The envconfig Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package envconfig
import (
"encoding/base64"
"encoding/hex"
"strings"
)
// Base64Bytes is a slice of bytes where the information is base64-encoded in
// the environment variable.
type Base64Bytes []byte
// EnvDecode implements env.Decoder.
func (b *Base64Bytes) EnvDecode(val string) error {
val = strings.ReplaceAll(val, "+", "-")
val = strings.ReplaceAll(val, "/", "_")
val = strings.TrimRight(val, "=")
var err error
*b, err = base64.RawURLEncoding.DecodeString(val)
return err
}
// Bytes returns the underlying bytes.
func (b Base64Bytes) Bytes() []byte {
return []byte(b)
}
// HexBytes is a slice of bytes where the information is hex-encoded in the
// environment variable.
type HexBytes []byte
// EnvDecode implements env.Decoder.
func (b *HexBytes) EnvDecode(val string) error {
var err error
*b, err = hex.DecodeString(val)
return err
}
// Bytes returns the underlying bytes.
func (b HexBytes) Bytes() []byte {
return []byte(b)
}

717
vendor/github.com/sethvargo/go-envconfig/envconfig.go generated vendored Normal file
View File

@@ -0,0 +1,717 @@
// Copyright The envconfig Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package envconfig populates struct fields based on environment variable
// values (or anything that responds to "Lookup"). Structs declare their
// environment dependencies using the "env" tag with the key being the name of
// the environment variable, case sensitive.
//
// type MyStruct struct {
// A string `env:"A"` // resolves A to $A
// B string `env:"B,required"` // resolves B to $B, errors if $B is unset
// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo"
//
// D string `env:"D,required,default=foo"` // error, cannot be required and default
// E string `env:""` // error, must specify key
// }
//
// All built-in types are supported except Func and Chan. If you need to define
// a custom decoder, implement Decoder:
//
// type MyStruct struct {
// field string
// }
//
// func (v *MyStruct) EnvDecode(val string) error {
// v.field = fmt.Sprintf("PREFIX-%s", val)
// return nil
// }
//
// In the environment, slices are specified as comma-separated values:
//
// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"}
//
// In the environment, maps are specified as comma-separated key:value pairs:
//
// export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"}
//
// If you need to modify environment variable values before processing, you can
// specify a custom mutator:
//
// type Config struct {
// Password `env:"PASSWORD_SECRET"`
// }
//
// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) {
// if strings.HasPrefix(value, "secret://") {
// return secretmanager.Resolve(ctx, value) // example
// }
// return value, nil
// }
//
// var config Config
// ProcessWith(&config, OsLookuper(), resolveSecretFunc)
package envconfig
import (
"context"
"encoding"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"strconv"
"strings"
"time"
)
const (
envTag = "env"
optDefault = "default="
optDelimiter = "delimiter="
optNoInit = "noinit"
optOverwrite = "overwrite"
optPrefix = "prefix="
optRequired = "required"
optSeparator = "separator="
defaultDelimiter = ","
defaultSeparator = ":"
)
// Error is a custom error type for errors returned by envconfig.
type Error string
// Error implements error.
func (e Error) Error() string {
return string(e)
}
const (
ErrInvalidEnvvarName = Error("invalid environment variable name")
ErrInvalidMapItem = Error("invalid map item")
ErrLookuperNil = Error("lookuper cannot be nil")
ErrMissingKey = Error("missing key")
ErrMissingRequired = Error("missing required value")
ErrNoInitNotPtr = Error("field must be a pointer to have noinit")
ErrNotPtr = Error("input must be a pointer")
ErrNotStruct = Error("input must be a struct")
ErrPrefixNotStruct = Error("prefix is only valid on struct types")
ErrPrivateField = Error("cannot parse private fields")
ErrRequiredAndDefault = Error("field cannot be required and have a default value")
ErrUnknownOption = Error("unknown option")
)
// Lookuper is an interface that provides a lookup for a string-based key.
type Lookuper interface {
// Lookup searches for the given key and returns the corresponding string
// value. If a value is found, it returns the value and true. If a value is
// not found, it returns the empty string and false.
Lookup(key string) (string, bool)
}
// osLookuper looks up environment configuration from the local environment.
type osLookuper struct{}
// Verify implements interface.
var _ Lookuper = (*osLookuper)(nil)
func (o *osLookuper) Lookup(key string) (string, bool) {
return os.LookupEnv(key)
}
// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to
// resolve values.
func OsLookuper() Lookuper {
return new(osLookuper)
}
type mapLookuper map[string]string
var _ Lookuper = (*mapLookuper)(nil)
func (m mapLookuper) Lookup(key string) (string, bool) {
v, ok := m[key]
return v, ok
}
// MapLookuper looks up environment configuration from a provided map. This is
// useful for testing, especially in parallel, since it does not require you to
// mutate the parent environment (which is stateful).
func MapLookuper(m map[string]string) Lookuper {
return mapLookuper(m)
}
type multiLookuper struct {
ls []Lookuper
}
var _ Lookuper = (*multiLookuper)(nil)
func (m *multiLookuper) Lookup(key string) (string, bool) {
for _, l := range m.ls {
if v, ok := l.Lookup(key); ok {
return v, true
}
}
return "", false
}
// PrefixLookuper looks up environment configuration using the specified prefix.
// This is useful if you want all your variables to start with a particular
// prefix like "MY_APP_".
func PrefixLookuper(prefix string, l Lookuper) Lookuper {
if typ, ok := l.(*prefixLookuper); ok {
return &prefixLookuper{prefix: typ.prefix + prefix, l: typ.l}
}
return &prefixLookuper{prefix: prefix, l: l}
}
type prefixLookuper struct {
l Lookuper
prefix string
}
func (p *prefixLookuper) Lookup(key string) (string, bool) {
return p.l.Lookup(p.prefix + key)
}
// MultiLookuper wraps a collection of lookupers. It does not combine them, and
// lookups appear in the order in which they are provided to the initializer.
func MultiLookuper(lookupers ...Lookuper) Lookuper {
return &multiLookuper{ls: lookupers}
}
// Decoder is an interface that custom types/fields can implement to control how
// decoding takes place. For example:
//
// type MyType string
//
// func (mt MyType) EnvDecode(val string) error {
// return "CUSTOM-"+val
// }
type Decoder interface {
EnvDecode(val string) error
}
// MutatorFunc is a function that mutates a given value before it is passed
// along for processing. This is useful if you want to mutate the environment
// variable value before it's converted to the proper type.
type MutatorFunc func(ctx context.Context, k, v string) (string, error)
// options are internal options for decoding.
type options struct {
Default string
Delimiter string
Prefix string
Separator string
NoInit bool
Overwrite bool
Required bool
}
// Process processes the struct using the environment. See [ProcessWith] for a
// more customizable version.
func Process(ctx context.Context, i interface{}) error {
return ProcessWith(ctx, i, OsLookuper())
}
// ProcessWith processes the given interface with the given lookuper. See the
// package-level documentation for specific examples and behaviors.
func ProcessWith(ctx context.Context, i interface{}, l Lookuper, fns ...MutatorFunc) error {
return processWith(ctx, i, l, false, fns...)
}
// processWith is a helper that captures whether the parent wanted
// initialization.
func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bool, fns ...MutatorFunc) error {
if l == nil {
return ErrLookuperNil
}
v := reflect.ValueOf(i)
if v.Kind() != reflect.Ptr {
return ErrNotPtr
}
e := v.Elem()
if e.Kind() != reflect.Struct {
return ErrNotStruct
}
t := e.Type()
for i := 0; i < t.NumField(); i++ {
ef := e.Field(i)
tf := t.Field(i)
tag := tf.Tag.Get(envTag)
if !ef.CanSet() {
if tag != "" {
// There's an "env" tag on a private field, we can't alter it, and it's
// likely a mistake. Return an error so the user can handle.
return fmt.Errorf("%s: %w", tf.Name, ErrPrivateField)
}
// Otherwise continue to the next field.
continue
}
// Parse the key and options.
key, opts, err := keyAndOpts(tag)
if err != nil {
return fmt.Errorf("%s: %w", tf.Name, err)
}
// NoInit is only permitted on pointers.
if opts.NoInit && ef.Kind() != reflect.Ptr {
return fmt.Errorf("%s: %w", tf.Name, ErrNoInitNotPtr)
}
shouldNotInit := opts.NoInit || parentNoInit
isNilStructPtr := false
setNilStruct := func(v reflect.Value) {
origin := e.Field(i)
if isNilStructPtr {
empty := reflect.New(origin.Type().Elem()).Interface()
// If a struct (after traversal) equals to the empty value, it means
// nothing was changed in any sub-fields. With the noinit opt, we skip
// setting the empty value to the original struct pointer (keep it nil).
if !reflect.DeepEqual(v.Interface(), empty) || !shouldNotInit {
origin.Set(v)
}
}
}
// Initialize pointer structs.
pointerWasSet := false
for ef.Kind() == reflect.Ptr {
if ef.IsNil() {
if ef.Type().Elem().Kind() != reflect.Struct {
// This is a nil pointer to something that isn't a struct, like
// *string. Move along.
break
}
isNilStructPtr = true
// Use an empty struct of the type so we can traverse.
ef = reflect.New(ef.Type().Elem()).Elem()
} else {
pointerWasSet = true
ef = ef.Elem()
}
}
// Special case handle structs. This has to come after the value resolution in
// case the struct has a custom decoder.
if ef.Kind() == reflect.Struct {
for ef.CanAddr() {
ef = ef.Addr()
}
// Lookup the value, ignoring an error if the key isn't defined. This is
// required for nested structs that don't declare their own `env` keys,
// but have internal fields with an `env` defined.
val, _, _, err := lookup(key, opts, l)
if err != nil && !errors.Is(err, ErrMissingKey) {
return fmt.Errorf("%s: %w", tf.Name, err)
}
if ok, err := processAsDecoder(val, ef); ok {
if err != nil {
return err
}
setNilStruct(ef)
continue
}
plu := l
if opts.Prefix != "" {
plu = PrefixLookuper(opts.Prefix, l)
}
if err := processWith(ctx, ef.Interface(), plu, shouldNotInit, fns...); err != nil {
return fmt.Errorf("%s: %w", tf.Name, err)
}
setNilStruct(ef)
continue
}
// It's invalid to have a prefix on a non-struct field.
if opts.Prefix != "" {
return ErrPrefixNotStruct
}
// Stop processing if there's no env tag (this comes after nested parsing),
// in case there's an env tag in an embedded struct.
if tag == "" {
continue
}
// The field already has a non-zero value and overwrite is false, do not
// overwrite.
if (pointerWasSet || !ef.IsZero()) && !opts.Overwrite {
continue
}
val, found, usedDefault, err := lookup(key, opts, l)
if err != nil {
return fmt.Errorf("%s: %w", tf.Name, err)
}
// If the field already has a non-zero value and there was no value directly
// specified, do not overwrite the existing field. We only want to overwrite
// when the envvar was provided directly.
if (pointerWasSet || !ef.IsZero()) && !found {
continue
}
// Apply any mutators. Mutators are applied after the lookup, but before any
// type conversions. They always resolve to a string (or error), so we don't
// call mutators when the environment variable was not set.
if found || usedDefault {
for _, fn := range fns {
if fn != nil {
val, err = fn(ctx, key, val)
if err != nil {
return fmt.Errorf("%s: %w", tf.Name, err)
}
}
}
}
// If Delimiter is not defined set it to ","
if opts.Delimiter == "" {
opts.Delimiter = defaultDelimiter
}
// If Separator is not defined set it to ":"
if opts.Separator == "" {
opts.Separator = defaultSeparator
}
// Set value.
if err := processField(val, ef, opts.Delimiter, opts.Separator, opts.NoInit); err != nil {
return fmt.Errorf("%s(%q): %w", tf.Name, val, err)
}
}
return nil
}
// keyAndOpts parses the given tag value (e.g. env:"foo,required") and
// returns the key name and options as a list.
func keyAndOpts(tag string) (string, *options, error) {
parts := strings.Split(tag, ",")
key, tagOpts := strings.TrimSpace(parts[0]), parts[1:]
if key != "" && !validateEnvName(key) {
return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName)
}
var opts options
LOOP:
for i, o := range tagOpts {
o = strings.TrimSpace(o)
switch {
case o == optOverwrite:
opts.Overwrite = true
case o == optRequired:
opts.Required = true
case o == optNoInit:
opts.NoInit = true
case strings.HasPrefix(o, optPrefix):
opts.Prefix = strings.TrimPrefix(o, optPrefix)
case strings.HasPrefix(o, optDelimiter):
opts.Delimiter = strings.TrimPrefix(o, optDelimiter)
case strings.HasPrefix(o, optSeparator):
opts.Separator = strings.TrimPrefix(o, optSeparator)
case strings.HasPrefix(o, optDefault):
// If a default value was given, assume everything after is the provided
// value, including comma-seprated items.
o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ")
opts.Default = strings.TrimPrefix(o, optDefault)
break LOOP
default:
return "", nil, fmt.Errorf("%q: %w", o, ErrUnknownOption)
}
}
return key, &opts, nil
}
// lookup looks up the given key using the provided Lookuper and options. The
// first boolean parameter indicates whether the value was found in the
// lookuper. The second boolean parameter indicates whether the default value
// was used.
func lookup(key string, opts *options, l Lookuper) (string, bool, bool, error) {
if key == "" {
// The struct has something like `env:",required"`, which is likely a
// mistake. We could try to infer the envvar from the field name, but that
// feels too magical.
return "", false, false, ErrMissingKey
}
if opts.Required && opts.Default != "" {
// Having a default value on a required value doesn't make sense.
return "", false, false, ErrRequiredAndDefault
}
// Lookup value.
val, found := l.Lookup(key)
if !found {
if opts.Required {
if pl, ok := l.(*prefixLookuper); ok {
key = pl.prefix + key
}
return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, key)
}
if opts.Default != "" {
// Expand the default value. This allows for a default value that maps to
// a different variable.
val = os.Expand(opts.Default, func(i string) string {
s, ok := l.Lookup(i)
if ok {
return s
}
return ""
})
return val, false, true, nil
}
}
return val, found, false, nil
}
// processAsDecoder processes the given value as a decoder or custom
// unmarshaller.
func processAsDecoder(v string, ef reflect.Value) (bool, error) {
// Keep a running error. It's possible that a property might implement
// multiple decoders, and we don't know *which* decoder will succeed. If we
// get through all of them, we'll return the most recent error.
var imp bool
var err error
// Resolve any pointers.
for ef.CanAddr() {
ef = ef.Addr()
}
if ef.CanInterface() {
iface := ef.Interface()
// If a developer chooses to implement the Decoder interface on a type,
// never attempt to use other decoders in case of failure. EnvDecode's
// decoding logic is "the right one", and the error returned (if any)
// is the most specific we can get.
if dec, ok := iface.(Decoder); ok {
imp = true
err = dec.EnvDecode(v)
return imp, err
}
if tu, ok := iface.(encoding.TextUnmarshaler); ok {
imp = true
if err = tu.UnmarshalText([]byte(v)); err == nil {
return imp, nil
}
}
if tu, ok := iface.(json.Unmarshaler); ok {
imp = true
if err = tu.UnmarshalJSON([]byte(v)); err == nil {
return imp, nil
}
}
if tu, ok := iface.(encoding.BinaryUnmarshaler); ok {
imp = true
if err = tu.UnmarshalBinary([]byte(v)); err == nil {
return imp, nil
}
}
if tu, ok := iface.(gob.GobDecoder); ok {
imp = true
if err = tu.GobDecode([]byte(v)); err == nil {
return imp, nil
}
}
}
return imp, err
}
func processField(v string, ef reflect.Value, delimiter, separator string, noInit bool) error {
// If the input value is empty and initialization is skipped, do nothing.
if v == "" && noInit {
return nil
}
// Handle pointers and uninitialized pointers.
for ef.Type().Kind() == reflect.Ptr {
if ef.IsNil() {
ef.Set(reflect.New(ef.Type().Elem()))
}
ef = ef.Elem()
}
tf := ef.Type()
tk := tf.Kind()
// Handle existing decoders.
if ok, err := processAsDecoder(v, ef); ok {
return err
}
// We don't check if the value is empty earlier, because the user might want
// to define a custom decoder and treat the empty variable as a special case.
// However, if we got this far, none of the remaining parsers will succeed, so
// bail out now.
if v == "" {
return nil
}
switch tk {
case reflect.Bool:
b, err := strconv.ParseBool(v)
if err != nil {
return err
}
ef.SetBool(b)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(v, tf.Bits())
if err != nil {
return err
}
ef.SetFloat(f)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
i, err := strconv.ParseInt(v, 0, tf.Bits())
if err != nil {
return err
}
ef.SetInt(i)
case reflect.Int64:
// Special case time.Duration values.
if tf.PkgPath() == "time" && tf.Name() == "Duration" {
d, err := time.ParseDuration(v)
if err != nil {
return err
}
ef.SetInt(int64(d))
} else {
i, err := strconv.ParseInt(v, 0, tf.Bits())
if err != nil {
return err
}
ef.SetInt(i)
}
case reflect.String:
ef.SetString(v)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
i, err := strconv.ParseUint(v, 0, tf.Bits())
if err != nil {
return err
}
ef.SetUint(i)
case reflect.Interface:
return fmt.Errorf("cannot decode into interfaces")
// Maps
case reflect.Map:
vals := strings.Split(v, delimiter)
mp := reflect.MakeMapWithSize(tf, len(vals))
for _, val := range vals {
pair := strings.SplitN(val, separator, 2)
if len(pair) < 2 {
return fmt.Errorf("%s: %w", val, ErrInvalidMapItem)
}
mKey, mVal := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1])
k := reflect.New(tf.Key()).Elem()
if err := processField(mKey, k, delimiter, separator, noInit); err != nil {
return fmt.Errorf("%s: %w", mKey, err)
}
v := reflect.New(tf.Elem()).Elem()
if err := processField(mVal, v, delimiter, separator, noInit); err != nil {
return fmt.Errorf("%s: %w", mVal, err)
}
mp.SetMapIndex(k, v)
}
ef.Set(mp)
// Slices
case reflect.Slice:
// Special case: []byte
if tf.Elem().Kind() == reflect.Uint8 {
ef.Set(reflect.ValueOf([]byte(v)))
} else {
vals := strings.Split(v, delimiter)
s := reflect.MakeSlice(tf, len(vals), len(vals))
for i, val := range vals {
val = strings.TrimSpace(val)
if err := processField(val, s.Index(i), delimiter, separator, noInit); err != nil {
return fmt.Errorf("%s: %w", val, err)
}
}
ef.Set(s)
}
}
return nil
}
// validateEnvName validates the given string conforms to being a valid
// environment variable.
//
// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase
// letters, digits, and _, and do not begin with a digit.
func validateEnvName(s string) bool {
if s == "" {
return false
}
for i, r := range s {
if (i == 0 && !isLetter(r)) || (!isLetter(r) && !isNumber(r) && r != '_') {
return false
}
}
return true
}
// isLetter returns true if the given rune is a letter between a-z,A-Z. This is
// different than unicode.IsLetter which includes all L character cases.
func isLetter(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
// isNumber returns true if the given run is a number between 0-9. This is
// different than unicode.IsNumber in that it only allows 0-9.
func isNumber(r rune) bool {
return r >= '0' && r <= '9'
}

5
vendor/modules.txt vendored
View File

@@ -410,6 +410,8 @@ github.com/json-iterator/go
# github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
## explicit
github.com/kballard/go-shellquote
# github.com/kelseyhightower/envconfig v1.4.0
## explicit
# github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351
## explicit
github.com/kevinburke/ssh_config
@@ -720,6 +722,9 @@ github.com/segmentio/backo-go
# github.com/sergi/go-diff v1.1.0
## explicit; go 1.12
github.com/sergi/go-diff/diffmatchpatch
# github.com/sethvargo/go-envconfig v0.8.2
## explicit; go 1.17
github.com/sethvargo/go-envconfig
# github.com/sirupsen/logrus v1.8.1
## explicit; go 1.13
github.com/sirupsen/logrus