...

Source file src/github.com/redhat-developer/odo/pkg/segment/context/context.go

Documentation: github.com/redhat-developer/odo/pkg/segment/context

     1  package context
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"hash/adler32"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/spf13/pflag"
    14  
    15  	"github.com/redhat-developer/odo/pkg/kclient"
    16  	"github.com/redhat-developer/odo/pkg/platform"
    17  	"github.com/redhat-developer/odo/pkg/podman"
    18  
    19  	dfutil "github.com/devfile/library/v2/pkg/util"
    20  
    21  	"k8s.io/klog"
    22  )
    23  
    24  const (
    25  	Caller                  = "caller"
    26  	ComponentType           = "componentType"
    27  	ClusterType             = "clusterType"
    28  	PreviousTelemetryStatus = "wasTelemetryEnabled"
    29  	TelemetryStatus         = "isTelemetryEnabled"
    30  	DevfileName             = "devfileName"
    31  	Language                = "language"
    32  	ProjectType             = "projectType"
    33  	NOTFOUND                = "not-found"
    34  	InteractiveMode         = "interactive"
    35  	ExperimentalMode        = "experimental"
    36  	Flags                   = "flags"
    37  	Platform                = "platform"
    38  	PlatformVersion         = "platformVersion"
    39  	PreferenceParameter     = "parameter"
    40  	PreferenceValue         = "value"
    41  )
    42  
    43  const (
    44  	VSCode   = "vscode"
    45  	IntelliJ = "intellij"
    46  	JBoss    = "jboss"
    47  )
    48  
    49  // Add the (case-insensitive) preference parameter name here to have the corresponding value sent verbatim to telemetry.
    50  var clearTextPreferenceParams = []string{
    51  	"ConsentTelemetry",
    52  	"Ephemeral",
    53  	"PushTimeout",
    54  	"RegistryCacheTime",
    55  	"Timeout",
    56  	"UpdateNotification",
    57  }
    58  
    59  type contextKey struct{}
    60  
    61  var key = contextKey{}
    62  
    63  // properties is a struct used to store data in a context and it comes with locking mechanism
    64  type properties struct {
    65  	lock    sync.Mutex
    66  	storage map[string]interface{}
    67  }
    68  
    69  // NewContext returns a context more specifically to be used for telemetry data collection
    70  func NewContext(ctx context.Context) context.Context {
    71  	return context.WithValue(ctx, key, &properties{storage: make(map[string]interface{})})
    72  }
    73  
    74  // GetContextProperties retrieves all the values set in a given context
    75  func GetContextProperties(ctx context.Context) map[string]interface{} {
    76  	cProperties := propertiesFromContext(ctx)
    77  	if cProperties == nil {
    78  		return make(map[string]interface{})
    79  	}
    80  	return cProperties.values()
    81  }
    82  
    83  // SetComponentType sets componentType property for telemetry data when a component is created/pushed
    84  func SetComponentType(ctx context.Context, value string) {
    85  	setContextProperty(ctx, ComponentType, dfutil.ExtractComponentType(value))
    86  }
    87  
    88  // SetClusterType sets clusterType property for telemetry data when a component is pushed or a project is created/set
    89  func SetClusterType(ctx context.Context, client kclient.ClientInterface) {
    90  	var value string
    91  	if client == nil {
    92  		value = NOTFOUND
    93  	} else {
    94  		// We are not checking ServerVersion to decide the cluster type because it does not always return the version,
    95  		// it sometimes fails to retrieve the data if user is using minishift or plain oc cluster
    96  		isOC, err := client.IsProjectSupported()
    97  		if err != nil {
    98  			klog.V(3).Info(fmt.Errorf("unable to detect project support: %w", err))
    99  			value = NOTFOUND
   100  		} else {
   101  			if isOC {
   102  				isOC4, err := client.IsCSVSupported()
   103  				// TODO: Add a unit test for this case
   104  				if err != nil {
   105  					value = "openshift"
   106  				} else {
   107  					if isOC4 {
   108  						value = "openshift4"
   109  					} else {
   110  						value = "openshift3"
   111  					}
   112  				}
   113  			} else {
   114  				value = "kubernetes"
   115  			}
   116  		}
   117  	}
   118  	setContextProperty(ctx, ClusterType, value)
   119  }
   120  
   121  // SetPlatform sets platform and platform_version properties for telemetry data
   122  func SetPlatform(ctx context.Context, client platform.Client) {
   123  	switch client := client.(type) {
   124  	case kclient.ClientInterface:
   125  		setPlatformCluster(ctx, client)
   126  	case podman.Client:
   127  		setPlatformPodman(ctx, client)
   128  	}
   129  }
   130  
   131  func setPlatformCluster(ctx context.Context, client kclient.ClientInterface) {
   132  	var value string
   133  	if client == nil {
   134  		value = NOTFOUND
   135  	} else {
   136  		// We are not checking ServerVersion to decide the cluster type because it does not always return the version,
   137  		// it sometimes fails to retrieve the data if user is using minishift or plain oc cluster
   138  
   139  		isOC, err := client.IsProjectSupported()
   140  		if err != nil {
   141  			klog.V(3).Info(fmt.Errorf("unable to detect project support: %w", err))
   142  			value = NOTFOUND
   143  		} else {
   144  			if isOC {
   145  				value = "openshift"
   146  				ocVersion, err := client.GetOCVersion()
   147  				if err == nil {
   148  					setContextProperty(ctx, PlatformVersion, ocVersion)
   149  				} else {
   150  					klog.V(3).Info(fmt.Errorf("unable to detect platform version: %w", err))
   151  				}
   152  			} else {
   153  				value = "kubernetes"
   154  				serverInfo, err := client.GetServerVersion(time.Second)
   155  				if err == nil {
   156  					setContextProperty(ctx, PlatformVersion, serverInfo.KubernetesVersion)
   157  				} else {
   158  					klog.V(3).Info(fmt.Errorf("unable to detect platform version: %w", err))
   159  				}
   160  			}
   161  		}
   162  	}
   163  	setContextProperty(ctx, Platform, value)
   164  }
   165  
   166  func setPlatformPodman(ctx context.Context, client podman.Client) {
   167  	setContextProperty(ctx, Platform, "podman")
   168  	version, err := client.Version(ctx)
   169  	if err != nil {
   170  		klog.V(3).Info(fmt.Errorf("unable to get podman version: %w", err))
   171  		return
   172  	}
   173  	setContextProperty(ctx, PlatformVersion, version.Client.Version)
   174  }
   175  
   176  // SetPreviousTelemetryStatus sets telemetry status before a command is run
   177  func SetPreviousTelemetryStatus(ctx context.Context, isEnabled bool) {
   178  	setContextProperty(ctx, PreviousTelemetryStatus, isEnabled)
   179  }
   180  
   181  // SetTelemetryStatus sets telemetry status after a command is run
   182  func SetTelemetryStatus(ctx context.Context, isEnabled bool) {
   183  	setContextProperty(ctx, TelemetryStatus, isEnabled)
   184  }
   185  
   186  func SetSignal(ctx context.Context, signal os.Signal) {
   187  	setContextProperty(ctx, "receivedSignal", signal.String())
   188  }
   189  
   190  func SetDevfileName(ctx context.Context, devfileName string) {
   191  	setContextProperty(ctx, DevfileName, devfileName)
   192  }
   193  
   194  func SetLanguage(ctx context.Context, language string) {
   195  	setContextProperty(ctx, Language, language)
   196  }
   197  
   198  func SetProjectType(ctx context.Context, projectType string) {
   199  	setContextProperty(ctx, ProjectType, projectType)
   200  }
   201  
   202  func SetInteractive(ctx context.Context, interactive bool) {
   203  	setContextProperty(ctx, InteractiveMode, interactive)
   204  }
   205  
   206  func SetExperimentalMode(ctx context.Context, value bool) {
   207  	setContextProperty(ctx, ExperimentalMode, value)
   208  }
   209  
   210  // SetFlags sets flags property for telemetry to record what flags were used
   211  func SetFlags(ctx context.Context, flags *pflag.FlagSet) {
   212  	var changedFlags []string
   213  	flags.VisitAll(func(f *pflag.Flag) {
   214  		if f.Changed {
   215  			if f.Name == "logtostderr" {
   216  				// skip "logtostderr" flag, for some reason it is showing as changed even when it is not
   217  				return
   218  			}
   219  			changedFlags = append(changedFlags, f.Name)
   220  		}
   221  	})
   222  	// the flags can't have spaces, so the output is space separated list of the flag names
   223  	setContextProperty(ctx, Flags, strings.Join(changedFlags, " "))
   224  }
   225  
   226  // SetCaller sets the caller property for telemetry to record the tool used to call odo.
   227  // Passing an empty caller is not considered invalid, but means that odo was invoked directly from the command line.
   228  // In all other cases, the value is verified against a set of allowed values.
   229  // Also note that unexpected values are added to the telemetry context, even if an error is returned.
   230  func SetCaller(ctx context.Context, caller string) error {
   231  	var err error
   232  	s := strings.TrimSpace(strings.ToLower(caller))
   233  	switch s {
   234  	case "", VSCode, IntelliJ, JBoss:
   235  		// An empty caller means that odo was invoked directly from the command line
   236  		err = nil
   237  	default:
   238  		// Note: we purposely don't disclose the list of allowed values
   239  		err = fmt.Errorf("unknown caller type: %q", caller)
   240  	}
   241  	setContextProperty(ctx, Caller, s)
   242  	return err
   243  }
   244  
   245  // SetPreferenceParameter tracks the preferences options usage, by recording both the parameter name and value.
   246  // By default, values are anonymized. Only parameters explicitly declared in the 'clearTextPreferenceParams' list will be recorded verbatim.
   247  // Setting value to nil means that the parameter has been unset in the preferences; so the value will not be recorded.
   248  func SetPreferenceParameter(ctx context.Context, param string, value *string) {
   249  	setContextProperty(ctx, PreferenceParameter, param)
   250  
   251  	if value == nil {
   252  		return
   253  	}
   254  
   255  	isClearTextParam := func() bool {
   256  		for _, clearTextParam := range clearTextPreferenceParams {
   257  			if strings.EqualFold(param, clearTextParam) {
   258  				return true
   259  			}
   260  		}
   261  		return false
   262  	}
   263  
   264  	recordedValue := *value
   265  	if !isClearTextParam() {
   266  		// adler32 for fast (and short) checksum computation, while minimizing the probability of collisions (which are not that important here).
   267  		// We just want to make sure that the same value returns the same anonymized string, while making it hard to guess the original string.
   268  		recordedValue = strconv.FormatUint(uint64(adler32.Checksum([]byte(recordedValue))), 16)
   269  	}
   270  	setContextProperty(ctx, PreferenceValue, recordedValue)
   271  }
   272  
   273  // GetPreviousTelemetryStatus gets the telemetry status that was seen before a command is run
   274  func GetPreviousTelemetryStatus(ctx context.Context) bool {
   275  	wasEnabled, ok := GetContextProperties(ctx)[PreviousTelemetryStatus]
   276  	if ok {
   277  		return wasEnabled.(bool)
   278  	}
   279  	return false
   280  }
   281  
   282  // GetTelemetryStatus gets the current telemetry status that is set after a command is run
   283  func GetTelemetryStatus(ctx context.Context) bool {
   284  	isEnabled, ok := GetContextProperties(ctx)[TelemetryStatus]
   285  	if ok {
   286  		return isEnabled.(bool)
   287  	}
   288  	return false
   289  }
   290  
   291  // set safely sets value for a key in storage
   292  func (p *properties) set(name string, value interface{}) {
   293  	p.lock.Lock()
   294  	defer p.lock.Unlock()
   295  	p.storage[name] = value
   296  }
   297  
   298  // values safely retrieves a deep copy of the storage
   299  func (p *properties) values() map[string]interface{} {
   300  	p.lock.Lock()
   301  	defer p.lock.Unlock()
   302  	ret := make(map[string]interface{})
   303  	for k, v := range p.storage {
   304  		ret[k] = v
   305  	}
   306  	return ret
   307  }
   308  
   309  // propertiesFromContext retrieves the properties instance from the context
   310  func propertiesFromContext(ctx context.Context) *properties {
   311  	value := ctx.Value(key)
   312  	if cast, ok := value.(*properties); ok {
   313  		return cast
   314  	}
   315  	return nil
   316  }
   317  
   318  // setContextProperty sets the value of a key in given context for telemetry data
   319  func setContextProperty(ctx context.Context, key string, value interface{}) {
   320  	cProperties := propertiesFromContext(ctx)
   321  	if cProperties != nil {
   322  		cProperties.set(key, value)
   323  	}
   324  }
   325  

View as plain text