diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go index 34620db870..0b784f4432 100644 --- a/cli/cmd/cluster.go +++ b/cli/cmd/cluster.go @@ -30,6 +30,7 @@ import ( "github.com/aws/aws-sdk-go/service/elbv2" "github.com/cortexlabs/cortex/cli/cluster" "github.com/cortexlabs/cortex/cli/types/cliconfig" + "github.com/cortexlabs/cortex/cli/types/flags" "github.com/cortexlabs/cortex/pkg/lib/archive" "github.com/cortexlabs/cortex/pkg/lib/aws" "github.com/cortexlabs/cortex/pkg/lib/console" @@ -37,6 +38,7 @@ import ( "github.com/cortexlabs/cortex/pkg/lib/errors" "github.com/cortexlabs/cortex/pkg/lib/exit" "github.com/cortexlabs/cortex/pkg/lib/files" + libjson "github.com/cortexlabs/cortex/pkg/lib/json" "github.com/cortexlabs/cortex/pkg/lib/pointer" "github.com/cortexlabs/cortex/pkg/lib/prompt" s "github.com/cortexlabs/cortex/pkg/lib/strings" @@ -76,6 +78,7 @@ func clusterInit() { addClusterConfigFlag(_clusterInfoCmd) addClusterNameFlag(_clusterInfoCmd) addClusterRegionFlag(_clusterInfoCmd) + _clusterInfoCmd.Flags().VarP(&_flagOutput, "output", "o", fmt.Sprintf("output format: one of %s", strings.Join(flags.UserOutputTypeStrings(), "|"))) _clusterInfoCmd.Flags().StringVarP(&_flagClusterInfoEnv, "configure-env", "e", "", "name of environment to configure") _clusterInfoCmd.Flags().BoolVarP(&_flagClusterInfoDebug, "debug", "d", false, "save the current cluster state to a file") _clusterInfoCmd.Flags().BoolVarP(&_flagClusterDisallowPrompt, "yes", "y", false, "skip prompts") @@ -158,7 +161,7 @@ var _clusterUpCmd = &cobra.Command{ exit.Error(err) } - awsClient, err := newAWSClient(accessConfig.Region) + awsClient, err := newAWSClient(accessConfig.Region, true) if err != nil { exit.Error(err) } @@ -276,7 +279,7 @@ var _clusterUpCmd = &cobra.Command{ exit.Error(ErrorClusterUp(out + helpStr)) } - loadBalancer, err := getAWSOperatorLoadBalancer(clusterConfig.ClusterName, awsClient) + loadBalancer, err := getLoadBalancer(clusterConfig.ClusterName, OperatorLoadBalancer, awsClient) if err != nil { exit.Error(errors.Append(err, fmt.Sprintf("\n\nyou can attempt to resolve this issue and configure your cli environment by running `cortex cluster info --configure-env %s`", _flagClusterUpEnv))) } @@ -326,7 +329,7 @@ var _clusterScaleCmd = &cobra.Command{ exit.Error(err) } - awsClient, err := newAWSClient(accessConfig.Region) + awsClient, err := newAWSClient(accessConfig.Region, true) if err != nil { exit.Error(err) } @@ -341,7 +344,7 @@ var _clusterScaleCmd = &cobra.Command{ exit.Error(err) } - clusterConfig := refreshCachedClusterConfig(*awsClient, accessConfig) + clusterConfig := refreshCachedClusterConfig(*awsClient, accessConfig, true) clusterConfig, err = updateNodeGroupScale(clusterConfig, _flagClusterScaleNodeGroup, scaleMinIntances, scaleMaxInstances, _flagClusterDisallowPrompt) if err != nil { exit.Error(err) @@ -380,15 +383,18 @@ var _clusterInfoCmd = &cobra.Command{ exit.Error(err) } - awsClient, err := newAWSClient(accessConfig.Region) + awsClient, err := newAWSClient(accessConfig.Region, _flagOutput == flags.PrettyOutputType) if err != nil { exit.Error(err) } if _flagClusterInfoDebug { + if _flagOutput != flags.PrettyOutputType { + exit.Error(ErrorJSONOutputNotSupportedWithFlag("--debug")) + } cmdDebug(awsClient, accessConfig) } else { - cmdInfo(awsClient, accessConfig, _flagClusterDisallowPrompt) + cmdInfo(awsClient, accessConfig, _flagOutput, _flagClusterDisallowPrompt) } }, } @@ -410,7 +416,7 @@ var _clusterDownCmd = &cobra.Command{ } // Check AWS access - awsClient, err := newAWSClient(accessConfig.Region) + awsClient, err := newAWSClient(accessConfig.Region, true) if err != nil { exit.Error(err) } @@ -447,7 +453,7 @@ var _clusterDownCmd = &cobra.Command{ } // updating CLI env is best-effort, so ignore errors - loadBalancer, _ := getAWSOperatorLoadBalancer(accessConfig.ClusterName, awsClient) + loadBalancer, _ := getLoadBalancer(accessConfig.ClusterName, OperatorLoadBalancer, awsClient) if _flagClusterDisallowPrompt { fmt.Printf("your cluster named \"%s\" in %s will be spun down and all apis will be deleted\n\n", accessConfig.ClusterName, accessConfig.Region) @@ -561,7 +567,7 @@ var _clusterExportCmd = &cobra.Command{ } // Check AWS access - awsClient, err := newAWSClient(accessConfig.Region) + awsClient, err := newAWSClient(accessConfig.Region, true) if err != nil { exit.Error(err) } @@ -577,7 +583,7 @@ var _clusterExportCmd = &cobra.Command{ exit.Error(err) } - loadBalancer, err := getAWSOperatorLoadBalancer(accessConfig.ClusterName, awsClient) + loadBalancer, err := getLoadBalancer(accessConfig.ClusterName, OperatorLoadBalancer, awsClient) if err != nil { exit.Error(err) } @@ -663,38 +669,60 @@ var _clusterExportCmd = &cobra.Command{ }, } -func cmdInfo(awsClient *aws.Client, accessConfig *clusterconfig.AccessConfig, disallowPrompt bool) { - if err := printInfoClusterState(awsClient, accessConfig); err != nil { - exit.Error(err) +func cmdInfo(awsClient *aws.Client, accessConfig *clusterconfig.AccessConfig, outputType flags.OutputType, disallowPrompt bool) { + if outputType == flags.PrettyOutputType { + if err := printInfoClusterState(awsClient, accessConfig); err != nil { + exit.Error(err) + } } - clusterConfig := refreshCachedClusterConfig(*awsClient, accessConfig) + clusterConfig := refreshCachedClusterConfig(*awsClient, accessConfig, outputType == flags.PrettyOutputType) - out, exitCode, err := runManagerWithClusterConfig("/root/info.sh", &clusterConfig, awsClient, nil, nil, nil) + operatorLoadBalancer, err := getLoadBalancer(accessConfig.ClusterName, OperatorLoadBalancer, awsClient) if err != nil { exit.Error(err) } - if exitCode == nil || *exitCode != 0 { - exit.Error(ErrorClusterInfo(out)) + apiLoadBalancer, err := getLoadBalancer(accessConfig.ClusterName, APILoadBalancer, awsClient) + if err != nil { + exit.Error(err) } - fmt.Println() + operatorEndpoint := s.EnsurePrefix(*operatorLoadBalancer.DNSName, "https://") + apiEndpoint := *apiLoadBalancer.DNSName - var operatorEndpoint string - for _, line := range strings.Split(out, "\n") { - // before modifying this, search for this prefix - if strings.HasPrefix(line, "operator: ") { - operatorEndpoint = "https://" + strings.TrimSpace(strings.TrimPrefix(line, "operator: ")) - break + if outputType == flags.JSONOutputType { + infoResponse, err := getInfoOperatorResponse(operatorEndpoint) + if err != nil { + exit.Error(err) } + infoResponse.ClusterConfig.Config = clusterConfig + + jsonBytes, err := libjson.Marshal(map[string]interface{}{ + "cluster_config": infoResponse.ClusterConfig.Config, + "cluster_metadata": infoResponse.ClusterConfig.OperatorMetadata, + "node_infos": infoResponse.NodeInfos, + "endpoint_operator": operatorEndpoint, + "endpoint_api": apiEndpoint, + }) + if err != nil { + exit.Error(err) + } + + fmt.Println(string(jsonBytes)) } + if outputType == flags.PrettyOutputType { + fmt.Println(console.Bold("endpoints:")) + fmt.Println("operator: ", operatorEndpoint) + fmt.Println("api load balancer:", apiEndpoint) + fmt.Println() - if err := printInfoOperatorResponse(clusterConfig, operatorEndpoint); err != nil { - exit.Error(err) + if err := printInfoOperatorResponse(clusterConfig, operatorEndpoint); err != nil { + exit.Error(err) + } } if _flagClusterInfoEnv != "" { - if err := updateAWSCLIEnv(_flagClusterInfoEnv, operatorEndpoint, disallowPrompt); err != nil { + if err := updateCLIEnv(_flagClusterInfoEnv, operatorEndpoint, disallowPrompt, outputType == flags.PrettyOutputType); err != nil { exit.Error(err) } } @@ -729,13 +757,7 @@ func printInfoOperatorResponse(clusterConfig clusterconfig.Config, operatorEndpo } yamlString := string(yamlBytes) - operatorConfig := cluster.OperatorConfig{ - Telemetry: isTelemetryEnabled(), - ClientID: clientID(), - OperatorEndpoint: operatorEndpoint, - } - - infoResponse, err := cluster.Info(operatorConfig) + infoResponse, err := getInfoOperatorResponse(operatorEndpoint) if err != nil { fmt.Println(yamlString) return err @@ -752,6 +774,15 @@ func printInfoOperatorResponse(clusterConfig clusterconfig.Config, operatorEndpo return nil } +func getInfoOperatorResponse(operatorEndpoint string) (*schema.InfoResponse, error) { + operatorConfig := cluster.OperatorConfig{ + Telemetry: isTelemetryEnabled(), + ClientID: clientID(), + OperatorEndpoint: operatorEndpoint, + } + return cluster.Info(operatorConfig) +} + func printInfoPricing(infoResponse *schema.InfoResponse, clusterConfig clusterconfig.Config) { eksPrice := aws.EKSPrices[clusterConfig.Region] operatorInstancePrice := aws.InstanceMetadatas[clusterConfig.Region]["t3.medium"].Price @@ -882,7 +913,7 @@ func printInfoNodes(infoResponse *schema.InfoResponse) { t.MustPrint(&table.Opts{Sort: pointer.Bool(false)}) } -func updateAWSCLIEnv(envName string, operatorEndpoint string, disallowPrompt bool) error { +func updateCLIEnv(envName string, operatorEndpoint string, disallowPrompt bool, printToStdout bool) error { prevEnv, err := readEnv(envName) if err != nil { return err @@ -897,14 +928,20 @@ func updateAWSCLIEnv(envName string, operatorEndpoint string, disallowPrompt boo envWasUpdated := false if prevEnv == nil { shouldWriteEnv = true - fmt.Println() + if printToStdout { + fmt.Println() + } } else if prevEnv.OperatorEndpoint != operatorEndpoint { envWasUpdated = true - if disallowPrompt { - shouldWriteEnv = true - fmt.Println() + if printToStdout { + if disallowPrompt { + shouldWriteEnv = true + fmt.Println() + } else { + shouldWriteEnv = prompt.YesOrNo(fmt.Sprintf("\nfound an existing environment named \"%s\"; would you like to overwrite it to connect to this cluster?", envName), "", "") + } } else { - shouldWriteEnv = prompt.YesOrNo(fmt.Sprintf("\nfound an existing environment named \"%s\"; would you like to overwrite it to connect to this cluster?", envName), "", "") + shouldWriteEnv = true } } @@ -914,10 +951,12 @@ func updateAWSCLIEnv(envName string, operatorEndpoint string, disallowPrompt boo return err } - if envWasUpdated { - fmt.Printf(console.Bold("the environment named \"%s\" has been updated to point to this cluster (and was set as the default environment)\n"), envName) - } else { - fmt.Printf(console.Bold("an environment named \"%s\" has been configured to point to this cluster (and was set as the default environment)\n"), envName) + if printToStdout { + if envWasUpdated { + fmt.Printf(console.Bold("the environment named \"%s\" has been updated to point to this cluster (and was set as the default environment)\n"), envName) + } else { + fmt.Printf(console.Bold("an environment named \"%s\" has been configured to point to this cluster (and was set as the default environment)\n"), envName) + } } } @@ -948,7 +987,7 @@ func cmdDebug(awsClient *aws.Client, accessConfig *clusterconfig.AccessConfig) { return } -func refreshCachedClusterConfig(awsClient aws.Client, accessConfig *clusterconfig.AccessConfig) clusterconfig.Config { +func refreshCachedClusterConfig(awsClient aws.Client, accessConfig *clusterconfig.AccessConfig, printToStdout bool) clusterconfig.Config { // add empty file if cached cluster doesn't exist so that the file output by manager container maintains current user permissions cachedClusterConfigPath := cachedClusterConfigPath(accessConfig.ClusterName, accessConfig.Region) containerConfigPath := fmt.Sprintf("/out/%s", filepath.Base(cachedClusterConfigPath)) @@ -960,7 +999,9 @@ func refreshCachedClusterConfig(awsClient aws.Client, accessConfig *clusterconfi }, } - fmt.Print("syncing cluster configuration ...\n\n") + if printToStdout { + fmt.Print("syncing cluster configuration ...\n\n") + } out, exitCode, err := runManagerAccessCommand("/root/refresh.sh "+containerConfigPath, *accessConfig, &awsClient, nil, copyFromPaths) if err != nil { exit.Error(err) @@ -1109,18 +1150,29 @@ func createLogGroupIfNotFound(awsClient *aws.Client, logGroup string, tags map[s return nil } -// Will return error if load balancer can't be found -func getAWSOperatorLoadBalancer(clusterName string, awsClient *aws.Client) (*elbv2.LoadBalancer, error) { +type LoadBalancer string + +var ( + OperatorLoadBalancer LoadBalancer = "operator" + APILoadBalancer LoadBalancer = "api" +) + +func (lb LoadBalancer) String() string { + return string(lb) +} + +// Will return error if the load balancer can't be found +func getLoadBalancer(clusterName string, whichLB LoadBalancer, awsClient *aws.Client) (*elbv2.LoadBalancer, error) { loadBalancer, err := awsClient.FindLoadBalancer(map[string]string{ clusterconfig.ClusterNameTag: clusterName, - "cortex.dev/load-balancer": "operator", + "cortex.dev/load-balancer": whichLB.String(), }) if err != nil { - return nil, errors.Wrap(err, "unable to locate operator load balancer") + return nil, errors.Wrap(err, fmt.Sprintf("unable to locate %s load balancer", whichLB.String())) } if loadBalancer == nil { - return nil, ErrorNoOperatorLoadBalancer() + return nil, ErrorNoOperatorLoadBalancer(whichLB.String()) } return loadBalancer, nil diff --git a/cli/cmd/errors.go b/cli/cmd/errors.go index fe38919556..db81a2744f 100644 --- a/cli/cmd/errors.go +++ b/cli/cmd/errors.go @@ -54,7 +54,6 @@ const ( ErrCredentialsInClusterConfig = "cli.credentials_in_cluster_config" ErrClusterUp = "cli.cluster_up" ErrClusterScale = "cli.cluster_scale" - ErrClusterInfo = "cli.cluster_info" ErrClusterDebug = "cli.cluster_debug" ErrClusterRefresh = "cli.cluster_refresh" ErrClusterDown = "cli.cluster_down" @@ -63,7 +62,7 @@ const ( ErrMaxInstancesLowerThan = "cli.max_instances_lower_than" ErrMinInstancesGreaterThanMaxInstances = "cli.min_instances_greater_than_max_instances" ErrNodeGroupNotFound = "cli.nodegroup_not_found" - ErrDuplicateCLIEnvNames = "cli.duplicate_cli_env_names" + ErrJSONOutputNotSupportedWithFlag = "cli.json_output_not_supported_with_flag" ErrClusterAccessConfigRequired = "cli.cluster_access_config_or_prompts_required" ErrShellCompletionNotSupported = "cli.shell_completion_not_supported" ErrNoTerminalWidth = "cli.no_terminal_width" @@ -119,10 +118,10 @@ func ErrorInvalidOperatorEndpoint(endpoint string) error { }) } -func ErrorNoOperatorLoadBalancer() error { +func ErrorNoOperatorLoadBalancer(whichLB string) error { return errors.WithStack(&errors.Error{ Kind: ErrNoOperatorLoadBalancer, - Message: "unable to locate operator load balancer", + Message: fmt.Sprintf("unable to locate %s load balancer", whichLB), }) } @@ -169,14 +168,6 @@ func ErrorClusterScale(out string) error { }) } -func ErrorClusterInfo(out string) error { - return errors.WithStack(&errors.Error{ - Kind: ErrClusterInfo, - Message: out, - NoPrint: true, - }) -} - func ErrorClusterDebug(out string) error { return errors.WithStack(&errors.Error{ Kind: ErrClusterDebug, @@ -236,6 +227,13 @@ func ErrorNodeGroupNotFound(scalingNodeGroupName, clusterName, clusterRegion str }) } +func ErrorJSONOutputNotSupportedWithFlag(flag string) error { + return errors.WithStack(&errors.Error{ + Kind: ErrJSONOutputNotSupportedWithFlag, + Message: fmt.Sprintf("flag %s cannot be used when output type is set to json", flag), + }) +} + func ErrorClusterAccessConfigRequired(cliFlagsOnly bool) error { message := "" if cliFlagsOnly { diff --git a/cli/cmd/lib_aws_creds.go b/cli/cmd/lib_aws_creds.go index 5d4abe3562..3bb847cda2 100644 --- a/cli/cmd/lib_aws_creds.go +++ b/cli/cmd/lib_aws_creds.go @@ -26,7 +26,7 @@ import ( "github.com/cortexlabs/cortex/pkg/types/clusterconfig" ) -func newAWSClient(region string) (*aws.Client, error) { +func newAWSClient(region string, printToStdout bool) (*aws.Client, error) { if err := clusterconfig.ValidateRegion(region); err != nil { return nil, err } @@ -40,7 +40,9 @@ func newAWSClient(region string) (*aws.Client, error) { return nil, err } - fmt.Println("using aws credentials with access key " + *awsClient.AccessKeyID() + "\n") + if printToStdout { + fmt.Println("using aws credentials with access key " + *awsClient.AccessKeyID() + "\n") + } return awsClient, nil } diff --git a/docs/clients/cli.md b/docs/clients/cli.md index e33bdd8271..6f5c1e6eda 100644 --- a/docs/clients/cli.md +++ b/docs/clients/cli.md @@ -118,6 +118,7 @@ Flags: -c, --config string path to a cluster configuration file -n, --name string name of the cluster -r, --region string aws region of the cluster + -o, --output string output format: one of pretty|json (default "pretty") -e, --configure-env string name of environment to configure -d, --debug save the current cluster state to a file -y, --yes skip prompts diff --git a/manager/info.sh b/manager/info.sh deleted file mode 100755 index 4dd7a83139..0000000000 --- a/manager/info.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Copyright 2021 Cortex Labs, Inc. -# -# 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. - -set -eo pipefail - -CORTEX_VERSION_MINOR=master - -function get_operator_endpoint() { - kubectl -n=istio-system get service ingressgateway-operator -o json | tr -d '[:space:]' | sed 's/.*{\"hostname\":\"\(.*\)\".*/\1/' -} - -function get_api_load_balancer_endpoint() { - kubectl -n=istio-system get service ingressgateway-apis -o json | tr -d '[:space:]' | sed 's/.*{\"hostname\":\"\(.*\)\".*/\1/' -} - -if ! eksctl utils describe-stacks --cluster=$CORTEX_CLUSTER_NAME --region=$CORTEX_REGION >/dev/null 2>&1; then - echo "error: there is no cluster named \"$CORTEX_CLUSTER_NAME\" in $CORTEX_REGION; please update your configuration to point to an existing cortex cluster or create a cortex cluster with \`cortex cluster up\`" - exit 1 -fi - -eksctl utils write-kubeconfig --cluster=$CORTEX_CLUSTER_NAME --region=$CORTEX_REGION | (grep -v "saved kubeconfig as" | grep -v "using region" | grep -v "eksctl version" || true) -out=$(kubectl get pods 2>&1 || true); if [[ "$out" == *"must be logged in to the server"* ]]; then echo "error: your aws iam user does not have access to this cluster; to grant access, see https://docs.cortex.dev/v/${CORTEX_VERSION_MINOR}/"; exit 1; fi - -operator_endpoint=$(get_operator_endpoint) -api_load_balancer_endpoint=$(get_api_load_balancer_endpoint) - -echo -e "\033[1mendpoints:\033[0m" -echo "operator: $operator_endpoint" # before modifying this, search for this prefix -echo "api load balancer: $api_load_balancer_endpoint"