diff --git a/cli/cmd/cloud.go b/cli/cmd/cloud.go new file mode 100644 index 000000000..0076a7750 --- /dev/null +++ b/cli/cmd/cloud.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/google/uuid" + "github.com/keyval-dev/odigos/cli/cmd/resources" + "github.com/keyval-dev/odigos/cli/pkg/kube" + "github.com/spf13/cobra" +) + +func verifyOdigosCloudApiKey(apikey string) error { + _, err := uuid.Parse(apikey) + if err != nil { + return fmt.Errorf("invalid apikey format. expected uuid format") + } + + return nil +} + +// cloudCmd represents the cloud command +var cloudCmd = &cobra.Command{ + Use: "cloud", + Short: "Manage odigos cloud", + Long: `Used to interact with odigos managed service.`, + Run: func(cmd *cobra.Command, args []string) { + client, err := kube.CreateClient(cmd) + if err != nil { + kube.PrintClientErrorAndExit(err) + } + ctx := cmd.Context() + + ns, err := resources.GetOdigosNamespace(client, ctx) + if resources.IsErrNoOdigosNamespaceFound(err) { + fmt.Println("\033[31mERROR\033[0m no odigos installation found in the current cluster. use \"odigos install\" to install odigos in the cluster or check that kubeconfig is pointing to the correct cluster.") + os.Exit(1) + } else if err != nil { + fmt.Printf("\033[31mERROR\033[0m Failed to check if Odigos is already installed: %s\n", err) + os.Exit(1) + } + + isOdigosCloud, err := resources.IsOdigosCloud(ctx, client, ns) + if err != nil { + fmt.Println("Odigos cloud failed - unable to read the current Odigos cloud configuration.") + os.Exit(1) + } + + if isOdigosCloud { + fmt.Println("Odigos cloud is currently enabled") + } else { + fmt.Println(`Odigos cloud is currently disabled. +To enable odigos cloud run 'odigos cloud login'`) + } + }, +} + +func init() { + rootCmd.AddCommand(cloudCmd) +} diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 6e046de96..5f634131e 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -79,6 +79,14 @@ This command will install k8s components that will auto-instrument your applicat // create resource managers specific for install isOdigosCloud := odigosCloudApiKeyFlag != "" + if isOdigosCloud { + err = verifyOdigosCloudApiKey(odigosCloudApiKeyFlag) + if err != nil { + fmt.Println("Odigos install failed - invalid api-key format.") + os.Exit(1) + } + } + resourceManagers := resources.CreateResourceManagers(client, ns, isOdigosCloud, &odigosCloudApiKeyFlag, &config) err = resources.ApplyResourceManagers(ctx, client, resourceManagers, "Creating") if err != nil { diff --git a/cli/cmd/login.go b/cli/cmd/login.go new file mode 100644 index 000000000..134397d25 --- /dev/null +++ b/cli/cmd/login.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "bufio" + "context" + "fmt" + "os" + "strconv" + + "github.com/keyval-dev/odigos/cli/cmd/resources" + "github.com/keyval-dev/odigos/cli/pkg/kube" + "github.com/keyval-dev/odigos/cli/pkg/labels" + "github.com/keyval-dev/odigos/cli/pkg/log" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// This function requires knowledge of which objects should be updated once the odigos cloud secret is updated. +// It is not very generic, but it is the best we can do for now. +func restartPodsAfterCloudLogin(ctx context.Context, client *kube.Client, ns string, configVersion int) error { + + configVersionStr := strconv.Itoa(configVersion) + patch := fmt.Sprintf(`{"spec":{"template":{"metadata":{"annotations":{"%s":"%s"}}}}}`, labels.OdigosSystemConfigLabelKey, configVersionStr) + + _, err := client.AppsV1().Deployments(ns).Patch(ctx, resources.KeyvalProxyDeploymentName, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) + if err != nil { + return err + } + + _, err = client.AppsV1().Deployments(ns).Patch(ctx, resources.OwnTelemetryCollectorDeploymentName, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) + if err != nil { + return err + } + + return nil +} + +// both login and update trigger this function. +func updateApiKey(cmd *cobra.Command, args []string) { + client, err := kube.CreateClient(cmd) + if err != nil { + kube.PrintClientErrorAndExit(err) + } + ctx := cmd.Context() + + ns, err := resources.GetOdigosNamespace(client, ctx) + if resources.IsErrNoOdigosNamespaceFound(err) { + fmt.Println("\033[31mERROR\033[0m no odigos installation found in the current cluster. use \"odigos install\" to install odigos in the cluster or check that kubeconfig is pointing to the correct cluster.") + os.Exit(1) + } else if err != nil { + fmt.Printf("\033[31mERROR\033[0m Failed to check if Odigos is already installed: %s\n", err) + os.Exit(1) + } + + config, err := resources.GetCurrentConfig(ctx, client, ns) + if err != nil { + fmt.Println("Odigos cloud login failed - unable to read the current Odigos configuration.") + os.Exit(1) + } + config.Spec.ConfigVersion += 1 + + if odigosCloudApiKeyFlag == "" { + fmt.Println("Enter your odigos cloud api-key. You can find it here: https://app.keyval.io/settings/api-keys") + fmt.Print("api-key: ") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + odigosCloudApiKeyFlag = scanner.Text() + } + + err = verifyOdigosCloudApiKey(odigosCloudApiKeyFlag) + if err != nil { + fmt.Println("Odigos cloud login failed - invalid api-key format.") + os.Exit(1) + } + + isPrevOdigosCloud, err := resources.IsOdigosCloud(ctx, client, ns) + if err != nil { + fmt.Println("Odigos cloud login failed - unable to read the current Odigos cloud configuration.") + os.Exit(1) + } + + resourceManagers := resources.CreateResourceManagers(client, ns, true, &odigosCloudApiKeyFlag, &config.Spec) + err = resources.ApplyResourceManagers(ctx, client, resourceManagers, "Updating") + if err != nil { + fmt.Println("Odigos cloud login failed - unable to apply Odigos resources.") + os.Exit(1) + } + err = resources.DeleteOldOdigosSystemObjects(ctx, client, ns, config) + if err != nil { + fmt.Println("Odigos cloud login failed - unable to cleanup old Odigos resources.") + os.Exit(1) + } + + if isPrevOdigosCloud { + l := log.Print("Restarting relevant pods ...") + err := restartPodsAfterCloudLogin(ctx, client, ns, config.Spec.ConfigVersion) + if err != nil { + l.Error(err) + } + l.Success() + } +} + +// loginCmd represents the login command +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to Odigos cloud", + Long: `Connect this Odigos installation to your odigos cloud account.`, + Run: updateApiKey, +} + +// loginCmd represents the login command +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update your Odigos Cloud api-key", + Long: `Use this command to update your Odigos Cloud api-key.`, + Run: updateApiKey, +} + +func init() { + cloudCmd.AddCommand(loginCmd) + cloudCmd.AddCommand(updateCmd) + + loginCmd.Flags().StringVarP(&odigosCloudApiKeyFlag, "api-key", "k", "", "api key for odigos cloud") + updateCmd.Flags().StringVarP(&odigosCloudApiKeyFlag, "api-key", "k", "", "api key for odigos cloud") +} diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go new file mode 100644 index 000000000..b24b7695a --- /dev/null +++ b/cli/cmd/logout.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/keyval-dev/odigos/cli/cmd/resources" + "github.com/keyval-dev/odigos/cli/pkg/confirm" + "github.com/keyval-dev/odigos/cli/pkg/kube" + "github.com/spf13/cobra" +) + +// logoutCmd represents the logout command +var logoutCmd = &cobra.Command{ + Use: "logout", + Short: "Logout from Odigos cloud", + Long: `Disconnect this Odigos installation from your odigos cloud account. + + After running this command, you will no longer be able to control and monitor this Odigos installation from Odigos cloud. + You can run 'odigos ui' to manage your Odigos installation locally. + `, + Run: func(cmd *cobra.Command, args []string) { + client, err := kube.CreateClient(cmd) + if err != nil { + kube.PrintClientErrorAndExit(err) + } + ctx := cmd.Context() + + ns, err := resources.GetOdigosNamespace(client, ctx) + if resources.IsErrNoOdigosNamespaceFound(err) { + fmt.Println("\033[31mERROR\033[0m no odigos installation found in the current cluster. use \"odigos install\" to install odigos in the cluster or check that kubeconfig is pointing to the correct cluster.") + os.Exit(1) + } else if err != nil { + fmt.Printf("\033[31mERROR\033[0m Failed to check if Odigos is already installed: %s\n", err) + os.Exit(1) + } + + isOdigosCloud, err := resources.IsOdigosCloud(ctx, client, ns) + if err != nil { + fmt.Println("Odigos cloud logout failed - unable to read the current Odigos cloud configuration.") + os.Exit(1) + } + if !isOdigosCloud { + fmt.Println("The current odigos installation is not connected to Odigos cloud.") + os.Exit(1) + } + + fmt.Println("About to logout from Odigos cloud. You can still manager your Odigos installation locally with 'odigos ui'.") + confirmed, err := confirm.Ask("Are you sure?") + if err != nil || !confirmed { + fmt.Println("Aborting odigos cloud logout") + return + } + + config, err := resources.GetCurrentConfig(ctx, client, ns) + if err != nil { + fmt.Println("Odigos cloud logout failed - unable to read the current Odigos configuration.") + os.Exit(1) + } + config.Spec.ConfigVersion += 1 + + emptyApiKey := "" + resourceManagers := resources.CreateResourceManagers(client, ns, false, &emptyApiKey, &config.Spec) + err = resources.ApplyResourceManagers(ctx, client, resourceManagers, "Updating") + if err != nil { + fmt.Println("Odigos cloud logout failed - unable to apply Odigos resources.") + os.Exit(1) + } + err = resources.DeleteOldOdigosSystemObjects(ctx, client, ns, config) + if err != nil { + fmt.Println("Odigos cloud logout failed - unable to cleanup old Odigos resources.") + os.Exit(1) + } + }, +} + +func init() { + cloudCmd.AddCommand(logoutCmd) +} diff --git a/cli/cmd/resources/keyvalproxy.go b/cli/cmd/resources/keyvalproxy.go index 82abc3416..de62342d9 100644 --- a/cli/cmd/resources/keyvalproxy.go +++ b/cli/cmd/resources/keyvalproxy.go @@ -24,7 +24,7 @@ const ( keyvalProxyServiceName = "odigos-cloud-k8s" keyvalProxyImage = "keyval/odigos-proxy-k8s" keyvalProxyAppName = "odigos-cloud-proxy" - keyvalProxyDeploymentName = "odigos-cloud-proxy" + KeyvalProxyDeploymentName = "odigos-cloud-proxy" keyvalProxyServiceAccountName = "odigos-cloud-proxy" keyvalProxyRoleName = "odigos-cloud-proxy" keyvalProxyRoleBindingName = "odigos-cloud-proxy" @@ -233,7 +233,7 @@ func NewKeyvalProxyDeployment(version string, ns string, imagePrefix string) *ap APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: keyvalProxyDeploymentName, + Name: KeyvalProxyDeploymentName, Namespace: ns, Labels: map[string]string{ "app": keyvalProxyAppName, diff --git a/cli/cmd/resources/owntelemetry.go b/cli/cmd/resources/owntelemetry.go index 292bd5795..ac3af1ad1 100644 --- a/cli/cmd/resources/owntelemetry.go +++ b/cli/cmd/resources/owntelemetry.go @@ -24,7 +24,7 @@ const ( ownTelemetryCollectorImage = "otel/opentelemetry-collector:0.86.0" ownTelemetryCollectorAppName = "own-telemetry-collector" ownTelemetryCollectorServiceName = "own-telemetry-collector" - ownTelemetryCollectorDeploymentName = "own-telemetry-collector" + OwnTelemetryCollectorDeploymentName = "own-telemetry-collector" ownTelemetryCollectorContainerName = "own-telemetry-collector" ownTelemetryCollectorConfigDir = "/etc/otelcol" // since we use otel/opentelemetry-collector which expect the image at this path ownTelemetryCollectorConfigConfigFileName = "config.yaml" // since we use otel/opentelemetry-collector which expect the config file to be called this way @@ -140,7 +140,7 @@ func NewOwnTelemetryCollectorDeployment(ns string) *appsv1.Deployment { APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: ownTelemetryCollectorDeploymentName, + Name: OwnTelemetryCollectorDeploymentName, Namespace: ns, Labels: map[string]string{ "app": ownTelemetryCollectorAppName,