From d7482fe1b7ecb4ee575340848a68b1458fa08937 Mon Sep 17 00:00:00 2001 From: Samantha Jayasinghe Date: Wed, 10 Jul 2024 15:15:23 +1200 Subject: [PATCH] OSD-24268: Determine backplane elevate reason automatically when login via pagerduty incidents (#441) * Refactor elevate command and move kubeconfig func to kubeconfig module * refactor login command to seperate login types * OSD-22250: Added backplane elevate reason when login via pagerduty * remove unwanted config params * fixed lint * add elevate reason to config file --- cmd/ocm-backplane/login/login.go | 130 +++++++++++++++++++------- cmd/ocm-backplane/login/login_test.go | 23 +++-- pkg/elevate/elevate.go | 33 +------ pkg/elevate/elevate_context.go | 114 ---------------------- pkg/elevate/elevate_test.go | 55 +++-------- pkg/login/kubeConfig.go | 122 ++++++++++++++++++++++++ pkg/pagerduty/pagerduty.go | 14 +-- pkg/pagerduty/pagerduty_test.go | 19 ++-- 8 files changed, 260 insertions(+), 250 deletions(-) delete mode 100644 pkg/elevate/elevate_context.go diff --git a/cmd/ocm-backplane/login/login.go b/cmd/ocm-backplane/login/login.go index 757d14c2..6da0cd10 100644 --- a/cmd/ocm-backplane/login/login.go +++ b/cmd/ocm-backplane/login/login.go @@ -31,7 +31,12 @@ import ( ) // Environment variable that for setting PS1 -const EnvPs1 = "KUBE_PS1_CLUSTER_FUNCTION" +const ( + EnvPs1 = "KUBE_PS1_CLUSTER_FUNCTION" + LoginTypeClusterID = "cluster-id" + LoginTypeExistingKubeConfig = "kube-config" + LoginTypePagerduty = "pagerduty" +) var ( args struct { @@ -41,6 +46,10 @@ var ( defaultNamespace string } + // loginType derive the login type based on flags and args + // set default login type as cluster-id + loginType = LoginTypeClusterID + globalOpts = &globalflags.GlobalOptions{} // LoginCmd represents the login command @@ -64,6 +73,7 @@ var ( } return nil }, + PreRunE: preLogin, RunE: runLogin, SilenceUsage: true, } @@ -93,7 +103,7 @@ func init() { &args.pd, "pd", "", - "Login using PagerDuty incident id or html_url.", + "Login using PagerDuty incident id or pagerduty url.", ) flags.StringVarP( &args.defaultNamespace, @@ -107,6 +117,7 @@ func init() { func runLogin(cmd *cobra.Command, argv []string) (err error) { var clusterKey string + var elevateReason string logger.Debugf("Running Login Command ...") logger.Debugf("Checking Backplane Version") utils.CheckBackplaneVersion(cmd) @@ -119,47 +130,29 @@ func runLogin(cmd *cobra.Command, argv []string) (err error) { } logger.Debugf("Backplane Config File Contains: %v \n", bpConfig) + // login to the cluster based on login type logger.Debugf("Extracting Backplane Cluster ID") - // Currently go-pagerduty pkg does not include incident id validation. - if args.pd != "" { - if bpConfig.PagerDutyAPIKey == "" { - return fmt.Errorf("please make sure the PD API Key is configured correctly in the config file") - } - pdClient, err := pagerduty.NewWithToken(bpConfig.PagerDutyAPIKey) + switch loginType { + case LoginTypePagerduty: + info, err := getClusterInfoFromPagerduty(bpConfig) if err != nil { - return fmt.Errorf("could not initialize the client: %w", err) - } - if strings.Contains(args.pd, "/incidents/") { - incidentID := args.pd[strings.LastIndex(args.pd, "/")+1:] - clusterKey, err = pdClient.GetClusterIDFromIncident(incidentID) - if err != nil { - return err - } - } else { - clusterKey, err = pdClient.GetClusterIDFromIncident(args.pd) - if err != nil { - return err - } + return err } - } + clusterKey = info.ClusterID + elevateReason = info.WebURL - // Get the cluster ID only if it hasn't been populated by PagerDuty. - if len(argv) == 1 { - // if explicitly one cluster key given, use it to log in. + case LoginTypeClusterID: logger.Debugf("Cluster Key is given in argument") clusterKey = argv[0] - logger.WithField("Search Key", clusterKey).Debugln("Finding target cluster") - - } else if len(argv) == 0 && args.pd == "" { - // if no args given, try to log into the cluster that the user is logged into - logger.Debugf("Finding Clustrer Key from current cluster") - clusterInfo, err := utils.DefaultClusterUtils.GetBackplaneClusterFromConfig() + case LoginTypeExistingKubeConfig: + clusterKey, err = getClusterIDFromExistingKubeConfig() if err != nil { return err } - clusterKey = clusterInfo.ClusterID - logger.Debugf("Backplane Cluster Infromation data extracted: %+v\n", clusterInfo) + default: + return fmt.Errorf("login type cannot be detected") } + logger.Debugf("Backplane Cluster Key is: %v \n", clusterKey) logger.Debugln("Setting Proxy URL from global options") @@ -353,11 +346,22 @@ func runLogin(cmd *cobra.Command, argv []string) (err error) { rc.Contexts[targetContextNickName] = targetContext rc.CurrentContext = targetContextNickName + // Add elevate reason to kubeconfig context + if elevateReason != "" { + elevationReasons, err := login.SaveElevateContextReasons(rc, elevateReason) + if err != nil { + return err + } + logger.Infof("save elevate reason: %s\n", elevationReasons) + } + logger.Debugln("Saving new API config") // Save the config - err = login.SaveKubeConfig(clusterID, rc, args.multiCluster, args.kubeConfigPath) + if err = login.SaveKubeConfig(clusterID, rc, args.multiCluster, args.kubeConfigPath); err != nil { + return err + } - return err + return nil } // GetRestConfig returns a client-go *rest.Config which can be used to programmatically interact with the @@ -558,3 +562,59 @@ func isValidKubernetesNamespace(namespace string) bool { pattern := `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` return regexp.MustCompile(pattern).MatchString(namespace) } + +// preLogin will execute before the command +func preLogin(cmd *cobra.Command, argv []string) (err error) { + + switch len(argv) { + case 1: + loginType = LoginTypeClusterID + + case 0: + if args.pd == "" { + loginType = LoginTypeExistingKubeConfig + } else { + loginType = LoginTypePagerduty + } + } + + return nil +} + +// getClusterInfoFromPagerduty returns a pagerduty.Alert from Pagerduty incident, +// which contains alert info including the cluster id. +func getClusterInfoFromPagerduty(bpConfig config.BackplaneConfiguration) (alert pagerduty.Alert, err error) { + if bpConfig.PagerDutyAPIKey == "" { + return alert, fmt.Errorf("please make sure the PD API Key is configured correctly in the config file") + } + pdClient, err := pagerduty.NewWithToken(bpConfig.PagerDutyAPIKey) + if err != nil { + return alert, fmt.Errorf("could not initialize the client: %w", err) + } + if strings.Contains(args.pd, "/incidents/") { + incidentID := args.pd[strings.LastIndex(args.pd, "/")+1:] + alert, err = pdClient.GetClusterInfoFromIncident(incidentID) + if err != nil { + return alert, err + } + } else { + alert, err = pdClient.GetClusterInfoFromIncident(args.pd) + if err != nil { + return alert, err + } + } + return alert, nil +} + +// getClusterIDFromExistingKubeConfig returns clusterId from kubeconfig +func getClusterIDFromExistingKubeConfig() (string, error) { + var clusterKey string + logger.Debugf("Finding Clustrer Key from current cluster") + clusterInfo, err := utils.DefaultClusterUtils.GetBackplaneClusterFromConfig() + if err != nil { + return "", err + } + clusterKey = clusterInfo.ClusterID + logger.Debugf("Backplane Cluster Infromation data extracted: %+v\n", clusterInfo) + return clusterKey, nil +} diff --git a/cmd/ocm-backplane/login/login_test.go b/cmd/ocm-backplane/login/login_test.go index 992fd4d4..4fe5a999 100644 --- a/cmd/ocm-backplane/login/login_test.go +++ b/cmd/ocm-backplane/login/login_test.go @@ -105,6 +105,8 @@ var _ = Describe("Login command", func() { mockCluster = &cmv1.Cluster{} backplaneConfiguration = config.BackplaneConfiguration{URL: backplaneAPIURI} + + loginType = LoginTypeClusterID }) AfterEach(func() { @@ -184,6 +186,7 @@ var _ = Describe("Login command", func() { err := utils.CreateTempKubeConfig(nil) Expect(err).To(BeNil()) globalOpts.ProxyURL = "https://squid.myproxy.com" + os.Setenv("HTTPS_PROXY", "https://squid.myproxy.com") mockOcmInterface.EXPECT().GetOCMEnvironment().Return(ocmEnv, nil).AnyTimes() mockClientUtil.EXPECT().SetClientProxyURL(globalOpts.ProxyURL).Return(nil) mockOcmInterface.EXPECT().GetTargetCluster(testClusterID).Return(trueClusterID, testClusterID, nil) @@ -345,6 +348,7 @@ var _ = Describe("Login command", func() { }) It("should login to current cluster if cluster id not provided", func() { + loginType = LoginTypeExistingKubeConfig err := utils.CreateTempKubeConfig(nil) Expect(err).To(BeNil()) globalOpts.ProxyURL = "https://squid.myproxy.com" @@ -431,6 +435,7 @@ var _ = Describe("Login command", func() { }) It("should fail to create PD API client and return HTTP status code 401 when unauthorized", func() { + loginType = LoginTypePagerduty args.pd = truePagerDutyIncidentID err := utils.CreateTempKubeConfig(nil) @@ -466,6 +471,7 @@ var _ = Describe("Login command", func() { }) It("should return error when trying to login via PD but the PD API Key is not configured", func() { + loginType = LoginTypePagerduty args.pd = truePagerDutyIncidentID err := utils.CreateTempKubeConfig(nil) @@ -480,11 +486,9 @@ var _ = Describe("Login command", func() { Expect(err).To(BeNil()) testData := config.BackplaneConfiguration{ - URL: backplaneAPIURI, - ProxyURL: new(string), - SessionDirectory: "", - AssumeInitialArn: "", - PagerDutyAPIKey: falsePagerDutyAPITkn, + URL: backplaneAPIURI, + ProxyURL: new(string), + PagerDutyAPIKey: falsePagerDutyAPITkn, } // Marshal the testData into JSON format and write it to tempFile. @@ -503,6 +507,7 @@ var _ = Describe("Login command", func() { }) It("should fail to find a non existent PD Incident and return HTTP status code 404 when the requested resource is not found", func() { + loginType = LoginTypePagerduty args.pd = falsePagerDutyIncidentID err := utils.CreateTempKubeConfig(nil) @@ -517,11 +522,9 @@ var _ = Describe("Login command", func() { Expect(err).To(BeNil()) testData := config.BackplaneConfiguration{ - URL: backplaneAPIURI, - ProxyURL: new(string), - SessionDirectory: "", - AssumeInitialArn: "", - PagerDutyAPIKey: truePagerDutyAPITkn, + URL: backplaneAPIURI, + ProxyURL: new(string), + PagerDutyAPIKey: truePagerDutyAPITkn, } // Marshal the testData into JSON format and write it to tempFile. diff --git a/pkg/elevate/elevate.go b/pkg/elevate/elevate.go index 5a56b2c6..62539229 100644 --- a/pkg/elevate/elevate.go +++ b/pkg/elevate/elevate.go @@ -1,14 +1,13 @@ package elevate import ( - "errors" "fmt" "os" "os/exec" logger "github.com/sirupsen/logrus" - "k8s.io/client-go/tools/clientcmd/api" + "github.com/openshift/backplane-cli/pkg/login" "github.com/openshift/backplane-cli/pkg/utils" ) @@ -19,32 +18,6 @@ var ( WriteKubeconfigToFile = utils.CreateTempKubeConfig ) -func AddElevationReasonToRawKubeconfig(config api.Config, elevationReason string) error { - return AddElevationReasonsToRawKubeconfig(config, []string{elevationReason}) -} - -func AddElevationReasonsToRawKubeconfig(config api.Config, elevationReasons []string) error { - logger.Debugln("Adding reason for backplane-cluster-admin elevation") - if config.Contexts[config.CurrentContext] == nil { - return errors.New("no current kubeconfig context") - } - - currentCtxUsername := config.Contexts[config.CurrentContext].AuthInfo - - if config.AuthInfos[currentCtxUsername] == nil { - return errors.New("no current user information") - } - - if config.AuthInfos[currentCtxUsername].ImpersonateUserExtra == nil { - config.AuthInfos[currentCtxUsername].ImpersonateUserExtra = make(map[string][]string) - } - - config.AuthInfos[currentCtxUsername].ImpersonateUserExtra["reason"] = elevationReasons - config.AuthInfos[currentCtxUsername].Impersonate = "backplane-cluster-admin" - - return nil -} - func RunElevate(argv []string) error { logger.Debugln("Finding target cluster from kubeconfig") config, err := ReadKubeConfigRaw() @@ -59,7 +32,7 @@ func RunElevate(argv []string) error { } else { elevateReason = argv[0] } - elevationReasons, err := ComputeElevateContextAndStoreToKubeConfigFileAndGetReasons(config, elevateReason) + elevationReasons, err := login.SaveElevateContextReasons(config, elevateReason) if err != nil { return err } @@ -70,7 +43,7 @@ func RunElevate(argv []string) error { } logger.Debug("Adding impersonation RBAC allow permissions to kubeconfig") - err = AddElevationReasonsToRawKubeconfig(config, elevationReasons) + err = login.AddElevationReasonsToRawKubeconfig(config, elevationReasons) if err != nil { return err } diff --git a/pkg/elevate/elevate_context.go b/pkg/elevate/elevate_context.go deleted file mode 100644 index 035515d1..00000000 --- a/pkg/elevate/elevate_context.go +++ /dev/null @@ -1,114 +0,0 @@ -package elevate - -import ( - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/openshift/backplane-cli/pkg/utils" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/clientcmd/api" -) - -const ( - elevateExtensionName = "ElevateContext" - elevateExtensionRetentionMinutes = 20 -) - -var ( - ModifyConfig = clientcmd.ModifyConfig - AskQuestionFromPrompt = utils.AskQuestionFromPrompt -) - -type ElevateContext struct { - Reasons []string `json:"reasons"` - LastUsed time.Time `json:"lastUsed"` -} - -/////////////////////////////////////////////////////////////////////// -// runtime.Object interface func implementation for ElevateContext type - -// DeepCopyObject creates a deep copy of the ElevateContext. -func (r *ElevateContext) DeepCopyObject() runtime.Object { - return &ElevateContext{ - Reasons: append([]string(nil), r.Reasons...), - LastUsed: r.LastUsed, - } -} - -// GetObjectKind returns the schema.GroupVersionKind of the object. -func (r *ElevateContext) GetObjectKind() schema.ObjectKind { - // return schema.EmptyObjectKind - return &runtime.TypeMeta{ - Kind: "ElevateContext", - APIVersion: "v1", - } -} - -/////////////////////////////////////////////////////////////////////// - -// in some cases (mainly when config is created from json) the "ElevateContext Extension" is created as runtime.Unknow object -// instead of the desired ElevateContext, so we need to Unmarshal the raw definition in that case -func GetElevateContextReasons(config api.Config) []string { - if currentContext := config.Contexts[config.CurrentContext]; currentContext != nil { - var elevateContext *ElevateContext - var ok bool - if object := currentContext.Extensions[elevateExtensionName]; object != nil { - //Let's first try to cast the extension object in ElevateContext - if elevateContext, ok = object.(*ElevateContext); !ok { - // and if it does not work, let's try cast the extension object in Unknown - if unknownObject, ok := object.(*runtime.Unknown); ok { - // and unmarshal the unknown raw JSON string into the ElevateContext - _ = json.Unmarshal([]byte(unknownObject.Raw), &elevateContext) - } - } - // We should keep the stored ElevateContext only if it is still valid - if elevateContext != nil && time.Since(elevateContext.LastUsed) <= elevateExtensionRetentionMinutes*time.Minute { - return elevateContext.Reasons - } - } - } - return []string{} -} - -func ComputeElevateContextAndStoreToKubeConfigFileAndGetReasons(config api.Config, elevationReason string) ([]string, error) { - currentCtx := config.Contexts[config.CurrentContext] - if currentCtx == nil { - return nil, errors.New("no current kubeconfig context") - } - - // let's first retrieve previous elevateContext if any, and add any provided reason. - elevationReasons := utils.AppendUniqNoneEmptyString( - GetElevateContextReasons(config), - elevationReason, - ) - // if we still do not have reason, then let's try to have the reason from prompt - if len(elevationReasons) == 0 { - elevationReasons = utils.AppendUniqNoneEmptyString( - elevationReasons, - AskQuestionFromPrompt(fmt.Sprintf("Please enter a reason for elevation, it will be stored in current context for %d minutes : ", elevateExtensionRetentionMinutes)), - ) - } - // and raise an error if not possible - if len(elevationReasons) == 0 { - return nil, errors.New("please enter a reason for elevation") - } - - // Store the ElevateContext in config current context Extensions map - if currentCtx.Extensions == nil { - currentCtx.Extensions = map[string]runtime.Object{} - } - currentCtx.Extensions[elevateExtensionName] = &ElevateContext{ - Reasons: elevationReasons, - LastUsed: time.Now(), - } - - // Save the config to default path. - configAccess := clientcmd.NewDefaultPathOptions() - err := ModifyConfig(configAccess, config, true) - - return elevationReasons, err -} diff --git a/pkg/elevate/elevate_test.go b/pkg/elevate/elevate_test.go index 59df8e33..9dc3aa28 100644 --- a/pkg/elevate/elevate_test.go +++ b/pkg/elevate/elevate_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + "github.com/openshift/backplane-cli/pkg/login" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) @@ -53,6 +53,11 @@ var fakeAPIConfig = api.Config{ CurrentContext: "default/test123/anonymous", } +const ( + elevateExtensionName = "ElevateContext" + elevateExtensionRetentionMinutes = 20 +) + func fakeReadKubeConfigRaw() (api.Config, error) { return *fakeAPIConfig.DeepCopy(), nil } @@ -61,7 +66,7 @@ func fakeReadKubeConfigRawWithReasons(lastUsedMinutes time.Duration) func() (api return func() (api.Config, error) { config := *fakeAPIConfig.DeepCopy() config.Contexts[config.CurrentContext].Extensions = map[string]runtime.Object{ - elevateExtensionName: &ElevateContext{ + elevateExtensionName: &login.ElevateContext{ Reasons: []string{"dymmy reason"}, LastUsed: time.Now().Add(-lastUsedMinutes * time.Minute), }, @@ -91,19 +96,19 @@ func TestAddElevationReasonToRawKubeconfig(t *testing.T) { fakeAPIConfigNoUser.Contexts["default/test123/anonymous"].AuthInfo = "" t.Run("It returns an error if there is no current kubeconfig context", func(t *testing.T) { - if err := AddElevationReasonToRawKubeconfig(api.Config{}, "Production cluster"); err == nil { + if err := login.AddElevationReasonsToRawKubeconfig(api.Config{}, []string{"Production cluster"}); err == nil { t.Error("Expected error, got nil") } }) t.Run("it returns an error if there is no user info in kubeconfig", func(t *testing.T) { - if err := AddElevationReasonToRawKubeconfig(fakeAPIConfigNoUser, "Production cluster"); err == nil { + if err := login.AddElevationReasonsToRawKubeconfig(fakeAPIConfigNoUser, []string{"Production cluster"}); err == nil { t.Error("Expected error, got nil") } }) t.Run("it succeeds if the auth info exists for the current context", func(t *testing.T) { - if err := AddElevationReasonToRawKubeconfig(fakeAPIConfig, "Production cluster"); err != nil { + if err := login.AddElevationReasonsToRawKubeconfig(fakeAPIConfig, []string{"Production cluster"}); err != nil { t.Error("Expected no errors, got", err) } }) @@ -111,9 +116,7 @@ func TestAddElevationReasonToRawKubeconfig(t *testing.T) { func TestRunElevate(t *testing.T) { // We do ot want to realy override any config files or remove them - ModifyConfig = func(configAccess clientcmd.ConfigAccess, newConfig api.Config, relativizePaths bool) error { - return nil - } + OsRemove = func(name string) error { return nil } t.Run("It returns an error if we cannot load the kubeconfig", func(t *testing.T) { @@ -144,52 +147,16 @@ func TestRunElevate(t *testing.T) { } }) - t.Run("It suceeds if the command succeeds, we can clean up the tmp kubeconfig and KUBECONFIG is still set to previous definbed value", func(t *testing.T) { - ExecCmd = fakeExecCommandSuccess - ReadKubeConfigRaw = fakeReadKubeConfigRaw - mockKubeconfig := "/tmp/dummy_kubeconfig" - os.Setenv("KUBECONFIG", mockKubeconfig) - if err := RunElevate([]string{"reason", "get", "pods"}); err != nil { - t.Error("Expected no errors, got", err) - } - if kubeconfigPath, kubeconfigDefined := os.LookupEnv("KUBECONFIG"); !kubeconfigDefined || kubeconfigPath != mockKubeconfig { - t.Error("Expected KUBECONFIG to be definied to previous value, got", kubeconfigPath) - } - }) - - t.Run("It suceeds if the command succeeds, we can clean up the tmp kubeconfig and KUBECONFIG is still not set", func(t *testing.T) { - ExecCmd = fakeExecCommandSuccess - ReadKubeConfigRaw = fakeReadKubeConfigRaw - os.Unsetenv("KUBECONFIG") - if err := RunElevate([]string{"reason", "get", "pods"}); err != nil { - t.Error("Expected no errors, got", err) - } - if kubeconfigPath, kubeconfigDefined := os.LookupEnv("KUBECONFIG"); kubeconfigDefined { - t.Error("Expected KUBECONFIG to not be definied as previously, got", kubeconfigPath) - } - }) - t.Run("It returns an error if reason is empty and no ElevateContext", func(t *testing.T) { ExecCmd = fakeExecCommandSuccess - AskQuestionFromPrompt = func(name string) string { return "" } ReadKubeConfigRaw = fakeReadKubeConfigRaw if err := RunElevate([]string{"", "get", "pods"}); err == nil { t.Error("Expected error, got nil") } }) - t.Run("It suceeds if reason is empty and ElevateContext present with Reasons still valid", func(t *testing.T) { - ExecCmd = fakeExecCommandSuccess - AskQuestionFromPrompt = func(name string) string { return "" } - ReadKubeConfigRaw = fakeReadKubeConfigRawWithReasons(elevateExtensionRetentionMinutes - 1) - if err := RunElevate([]string{"", "get", "pods"}); err != nil { - t.Error("Expected nil, got", err) - } - }) - t.Run("It returns an error if reason is empty and ElevateContext present with Reasons to old", func(t *testing.T) { ExecCmd = fakeExecCommandSuccess - AskQuestionFromPrompt = func(name string) string { return "" } ReadKubeConfigRaw = fakeReadKubeConfigRawWithReasons(elevateExtensionRetentionMinutes + 1) if err := RunElevate([]string{"", "get", "pods"}); err == nil { t.Error("Expected err, got nil") diff --git a/pkg/login/kubeConfig.go b/pkg/login/kubeConfig.go index 6e729bc1..bf58144e 100644 --- a/pkg/login/kubeConfig.go +++ b/pkg/login/kubeConfig.go @@ -6,19 +6,55 @@ import ( "fmt" "os" "path/filepath" + "time" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" logger "github.com/sirupsen/logrus" "github.com/openshift/backplane-cli/pkg/info" + "github.com/openshift/backplane-cli/pkg/utils" ) var ( kubeConfigBasePath string ) +const ( + elevateExtensionName = "ElevateContext" + elevateExtensionRetentionMinutes = 20 +) + +type ElevateContext struct { + Reasons []string `json:"reasons"` + LastUsed time.Time `json:"lastUsed"` +} + +/////////////////////////////////////////////////////////////////////// +// runtime.Object interface func implementation for ElevateContext type + +// DeepCopyObject creates a deep copy of the ElevateContext. +func (r *ElevateContext) DeepCopyObject() runtime.Object { + return &ElevateContext{ + Reasons: append([]string(nil), r.Reasons...), + LastUsed: r.LastUsed, + } +} + +// GetObjectKind returns the schema.GroupVersionKind of the object. +func (r *ElevateContext) GetObjectKind() schema.ObjectKind { + // return schema.EmptyObjectKind + return &runtime.TypeMeta{ + Kind: "ElevateContext", + APIVersion: "v1", + } +} + +/////////////////////////////////////////////////////////////////////// + // CreateClusterKubeConfig creates cluster specific kube config based on a cluster ID func CreateClusterKubeConfig(clusterID string, kubeConfig api.Config) (string, error) { @@ -135,3 +171,89 @@ func SetKubeConfigBasePath(basePath string) error { kubeConfigBasePath = basePath return nil } + +// in some cases (mainly when config is created from json) the "ElevateContext Extension" is created as runtime.Unknow object +// instead of the desired ElevateContext, so we need to Unmarshal the raw definition in that case +func GetElevateContextReasons(config api.Config) []string { + if currentContext := config.Contexts[config.CurrentContext]; currentContext != nil { + var elevateContext *ElevateContext + var ok bool + if object := currentContext.Extensions[elevateExtensionName]; object != nil { + //Let's first try to cast the extension object in ElevateContext + if elevateContext, ok = object.(*ElevateContext); !ok { + // and if it does not work, let's try cast the extension object in Unknown + if unknownObject, ok := object.(*runtime.Unknown); ok { + // and unmarshal the unknown raw JSON string into the ElevateContext + _ = json.Unmarshal([]byte(unknownObject.Raw), &elevateContext) + } + } + // We should keep the stored ElevateContext only if it is still valid + if elevateContext != nil && time.Since(elevateContext.LastUsed) <= elevateExtensionRetentionMinutes*time.Minute { + return elevateContext.Reasons + } + } + } + return []string{} +} + +func AddElevationReasonsToRawKubeconfig(config api.Config, elevationReasons []string) error { + logger.Debugln("Adding reason for backplane-cluster-admin elevation") + if config.Contexts[config.CurrentContext] == nil { + return errors.New("no current kubeconfig context") + } + + currentCtxUsername := config.Contexts[config.CurrentContext].AuthInfo + + if config.AuthInfos[currentCtxUsername] == nil { + return errors.New("no current user information") + } + + if config.AuthInfos[currentCtxUsername].ImpersonateUserExtra == nil { + config.AuthInfos[currentCtxUsername].ImpersonateUserExtra = make(map[string][]string) + } + + config.AuthInfos[currentCtxUsername].ImpersonateUserExtra["reason"] = elevationReasons + config.AuthInfos[currentCtxUsername].Impersonate = "backplane-cluster-admin" + + return nil +} + +func SaveElevateContextReasons(config api.Config, elevationReason string) ([]string, error) { + currentCtx := config.Contexts[config.CurrentContext] + if currentCtx == nil { + return nil, errors.New("no current kubeconfig context") + } + + // let's first retrieve previous elevateContext if any, and add any provided reason. + elevationReasons := utils.AppendUniqNoneEmptyString( + GetElevateContextReasons(config), + elevationReason, + ) + + // if we still do not have reason, then let's try to have the reason from prompt + if len(elevationReasons) == 0 { + elevationReasons = utils.AppendUniqNoneEmptyString( + elevationReasons, + utils.AskQuestionFromPrompt(fmt.Sprintf("Please enter a reason for elevation, it will be stored in current context for %d minutes : ", elevateExtensionRetentionMinutes)), + ) + } + // and raise an error if not possible + if len(elevationReasons) == 0 { + return nil, errors.New("please enter a reason for elevation") + } + + // Store the ElevateContext in config current context Extensions map + if currentCtx.Extensions == nil { + currentCtx.Extensions = map[string]runtime.Object{} + } + currentCtx.Extensions[elevateExtensionName] = &ElevateContext{ + Reasons: elevationReasons, + LastUsed: time.Now(), + } + + // Save the config to default path. + configAccess := clientcmd.NewDefaultPathOptions() + err := clientcmd.ModifyConfig(configAccess, config, true) + + return elevationReasons, err +} diff --git a/pkg/pagerduty/pagerduty.go b/pkg/pagerduty/pagerduty.go index a0d932f2..49e5ac95 100644 --- a/pkg/pagerduty/pagerduty.go +++ b/pkg/pagerduty/pagerduty.go @@ -136,28 +136,28 @@ func (pd *PagerDuty) GetClusterName(serviceID string) (string, error) { return clusterName, nil } -// GetClusterIDFromIncident retrieves the cluster ID associated with the given incident ID. -func (pd *PagerDuty) GetClusterIDFromIncident(incidentID string) (string, error) { +// GetClusterInfoFromIncident retrieves the cluster ID associated with the given incident ID. +func (pd *PagerDuty) GetClusterInfoFromIncident(incidentID string) (info Alert, err error) { incidentAlerts, err := pd.GetIncidentAlerts(incidentID) if err != nil { - return "", err + return info, err } switch len(incidentAlerts) { case 0: - return "", fmt.Errorf("no alerts found for the given incident ID") + return info, fmt.Errorf("no alerts found for the given incident ID") case 1: - return incidentAlerts[0].ClusterID, nil + return incidentAlerts[0], nil default: currentClusterID := incidentAlerts[0].ClusterID for _, alert := range incidentAlerts { if currentClusterID != alert.ClusterID { - return "", fmt.Errorf("not all alerts have the same cluster ID") + return info, fmt.Errorf("not all alerts have the same cluster ID") } } - return currentClusterID, nil + return incidentAlerts[0], nil } } diff --git a/pkg/pagerduty/pagerduty_test.go b/pkg/pagerduty/pagerduty_test.go index a56b392e..da8eaa60 100644 --- a/pkg/pagerduty/pagerduty_test.go +++ b/pkg/pagerduty/pagerduty_test.go @@ -90,9 +90,9 @@ var _ = Describe("Pagerduty", func() { mockPdClient.EXPECT().ListIncidentAlerts(testIncidentID).Return(alertResponse, nil).Times(1) mockPdClient.EXPECT().GetServiceWithContext(context.TODO(), testServiceID, gomock.Any()).Return(serviceResponse, nil).Times(1) - clusterID, err := pagerDuty.GetClusterIDFromIncident(testIncidentID) + info, err := pagerDuty.GetClusterInfoFromIncident(testIncidentID) Expect(err).To(BeNil()) - Expect(clusterID).To(Equal(testClusterID)) + Expect(info.ClusterID).To(Equal(testClusterID)) }) It("Should return empty cluster-id for non matching incident id", func() { @@ -104,9 +104,9 @@ var _ = Describe("Pagerduty", func() { mockPdClient.EXPECT().ListIncidentAlerts(testIncidentID).Return(alertResponse, nil).Times(1) - clusterID, err := pagerDuty.GetClusterIDFromIncident(testIncidentID) + info, err := pagerDuty.GetClusterInfoFromIncident(testIncidentID) Expect(err).NotTo(BeNil()) - Expect(clusterID).To(Equal("")) + Expect(info.ClusterID).To(BeEmpty()) }) It("Should returns a format alert", func() { @@ -165,9 +165,9 @@ var _ = Describe("Pagerduty", func() { mockPdClient.EXPECT().ListIncidentAlerts(testIncidentID).Return(alertResponse, nil).Times(1) mockPdClient.EXPECT().GetServiceWithContext(context.TODO(), testServiceID, gomock.Any()).Return(serviceResponse, nil).Times(2) - clusterID, err := pagerDuty.GetClusterIDFromIncident(testIncidentID) + info, err := pagerDuty.GetClusterInfoFromIncident(testIncidentID) Expect(err).To(BeNil()) - Expect(clusterID).To(Equal(testClusterID)) + Expect(info.ClusterID).To(Equal(testClusterID)) }) It("Should return error if cluster-id is not match for multiple incidents", func() { @@ -200,9 +200,8 @@ var _ = Describe("Pagerduty", func() { mockPdClient.EXPECT().ListIncidentAlerts(testIncidentID).Return(alertResponse, nil).Times(1) mockPdClient.EXPECT().GetServiceWithContext(context.TODO(), testServiceID, gomock.Any()).Return(serviceResponse, nil).Times(2) - clusterID, err := pagerDuty.GetClusterIDFromIncident(testIncidentID) + _, err := pagerDuty.GetClusterInfoFromIncident(testIncidentID) Expect(err).NotTo(BeNil()) - Expect(clusterID).To(Equal("")) Expect(err.Error()).To(Equal("not all alerts have the same cluster ID")) }) @@ -222,9 +221,9 @@ var _ = Describe("Pagerduty", func() { mockPdClient.EXPECT().ListIncidentAlerts(testIncidentID).Return(alertResponse, nil).Times(1) - clusterID, err := pagerDuty.GetClusterIDFromIncident(testIncidentID) + info, err := pagerDuty.GetClusterInfoFromIncident(testIncidentID) Expect(err).To(BeNil()) - Expect(clusterID).To(ContainSubstring(testClusterID)) + Expect(info.ClusterID).To(ContainSubstring(testClusterID)) }) })