github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/context/cloud_context.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package context
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"net/http"
    26  	"os"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/jedib0t/go-pretty/v6/table"
    31  	"github.com/pkg/errors"
    32  	"gopkg.in/yaml.v2"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  
    35  	"github.com/1aal/kubeblocks/pkg/cli/cmd/organization"
    36  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    37  )
    38  
    39  type CloudContext struct {
    40  	ContextName  string
    41  	Token        string
    42  	OrgName      string
    43  	APIURL       string
    44  	APIPath      string
    45  	OutputFormat string
    46  
    47  	genericiooptions.IOStreams
    48  }
    49  
    50  type Metadata struct {
    51  	Name         string `json:"name"`
    52  	Description  string `json:"description"`
    53  	Project      string `json:"project"`
    54  	Organization string `json:"organization"`
    55  	Partner      string `json:"partner"`
    56  	ID           string `json:"id"`
    57  	CreatedAt    struct {
    58  		Seconds int `json:"seconds"`
    59  		Nanos   int `json:"nanos"`
    60  	} `json:"createdAt"`
    61  	ModifiedAt struct {
    62  		Seconds int `json:"seconds"`
    63  		Nanos   int `json:"nanos"`
    64  	} `json:"modifiedAt"`
    65  }
    66  
    67  type Params struct {
    68  	KubernetesProvider   string `json:"kubernetesProvider"`
    69  	ProvisionEnvironment string `json:"provisionEnvironment"`
    70  	ProvisionType        string `json:"provisionType"`
    71  	State                string `json:"state"`
    72  }
    73  
    74  type ClusterStatus struct {
    75  	Conditions []struct {
    76  		Type        string `json:"type"`
    77  		LastUpdated struct {
    78  			Seconds int `json:"seconds"`
    79  			Nanos   int `json:"nanos"`
    80  		} `json:"lastUpdated"`
    81  		Reason string `json:"reason"`
    82  	} `json:"conditions"`
    83  	Token              string `json:"token"`
    84  	PublishedBlueprint string `json:"publishedBlueprint"`
    85  }
    86  
    87  type ClusterData struct {
    88  	ClusterBlueprint string `json:"cluster_blueprint"`
    89  	Projects         []struct {
    90  		ProjectID string `json:"projectID"`
    91  		ClusterID string `json:"clusterID"`
    92  	} `json:"projects"`
    93  	ClusterStatus ClusterStatus `json:"cluster_status"`
    94  }
    95  
    96  type ClusterSpec struct {
    97  	ClusterType      string      `json:"clusterType"`
    98  	Metro            struct{}    `json:"metro"`
    99  	OverrideSelector string      `json:"overrideSelector"`
   100  	Params           Params      `json:"params"`
   101  	ProxyConfig      struct{}    `json:"proxyConfig"`
   102  	ClusterData      ClusterData `json:"clusterData"`
   103  }
   104  
   105  type ClusterItem struct {
   106  	Metadata Metadata    `json:"metadata"`
   107  	Spec     ClusterSpec `json:"spec"`
   108  }
   109  
   110  type CloudContextsResponse struct {
   111  	APIVersion string `json:"apiVersion"`
   112  	Kind       string `json:"kind"`
   113  	Metadata   struct {
   114  		Count int `json:"count"`
   115  		Limit int `json:"limit"`
   116  	} `json:"metadata"`
   117  	Items []ClusterItem `json:"items"`
   118  }
   119  
   120  type Status struct {
   121  	ConditionStatus int `json:"conditionStatus"`
   122  }
   123  
   124  type CloudContextResponse struct {
   125  	APIVersion string      `json:"apiVersion"`
   126  	Kind       string      `json:"kind"`
   127  	Metadata   Metadata    `json:"metadata"`
   128  	Spec       ClusterSpec `json:"spec"`
   129  	Status     Status      `json:"status"`
   130  }
   131  
   132  func (c *CloudContext) showContext() error {
   133  	cloudContext, err := c.GetContext()
   134  	if err != nil {
   135  		return errors.Wrapf(err, "Failed to get context %s", c.ContextName)
   136  	}
   137  
   138  	switch strings.ToLower(c.OutputFormat) {
   139  	case "yaml":
   140  		return c.printYAML(cloudContext)
   141  	case "json":
   142  		return c.printJSON(cloudContext)
   143  	case "human":
   144  		fallthrough
   145  	default:
   146  		return c.printTable(cloudContext)
   147  	}
   148  }
   149  
   150  func (c *CloudContext) printYAML(ctxRes *CloudContextResponse) error {
   151  	yamlData, err := yaml.Marshal(ctxRes)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	fmt.Fprintf(c.Out, "%s", string(yamlData))
   157  	return nil
   158  }
   159  
   160  func (c *CloudContext) printJSON(ctxRes *CloudContextResponse) error {
   161  	jsonData, err := json.MarshalIndent(ctxRes, "", "    ")
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	fmt.Fprintf(c.Out, "%s", string(jsonData))
   167  	return nil
   168  }
   169  
   170  func (c *CloudContext) printTable(ctxRes *CloudContextResponse) error {
   171  	tbl := printer.NewTablePrinter(c.Out)
   172  	tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{
   173  		{Number: 8, WidthMax: 120},
   174  	})
   175  	tbl.SetHeader(
   176  		"NAME",
   177  		"Description",
   178  		"Project",
   179  		"Organization",
   180  		"Partner",
   181  		"ID",
   182  		"CreatedAt",
   183  		"ModifiedAt",
   184  	)
   185  
   186  	createAt := convertTimestampToHumanReadable(
   187  		ctxRes.Metadata.CreatedAt.Seconds,
   188  		ctxRes.Metadata.CreatedAt.Nanos,
   189  	)
   190  	modifiedAt := convertTimestampToHumanReadable(
   191  		ctxRes.Metadata.ModifiedAt.Seconds,
   192  		ctxRes.Metadata.ModifiedAt.Nanos,
   193  	)
   194  	tbl.AddRow(
   195  		ctxRes.Metadata.Name,
   196  		ctxRes.Metadata.Description,
   197  		ctxRes.Metadata.Project,
   198  		ctxRes.Metadata.Organization,
   199  		ctxRes.Metadata.Partner,
   200  		ctxRes.Metadata.ID,
   201  		createAt,
   202  		modifiedAt,
   203  	)
   204  	tbl.Print()
   205  
   206  	return nil
   207  }
   208  
   209  func (c *CloudContext) showContexts() error {
   210  	cloudContexts, err := c.GetContexts()
   211  	if err != nil {
   212  		return errors.Wrapf(err, "Failed to get contexts, please check your organization name")
   213  	}
   214  
   215  	tbl := printer.NewTablePrinter(c.Out)
   216  	tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{
   217  		{Number: 8, WidthMax: 120},
   218  	})
   219  	tbl.SetHeader(
   220  		"NAME",
   221  		"Description",
   222  		"Project",
   223  		"Organization",
   224  		"Partner",
   225  		"ID",
   226  		"CreatedAt",
   227  		"ModifiedAt",
   228  	)
   229  
   230  	for _, orgItem := range cloudContexts.Items {
   231  		createAt := convertTimestampToHumanReadable(
   232  			orgItem.Metadata.CreatedAt.Seconds,
   233  			orgItem.Metadata.CreatedAt.Nanos,
   234  		)
   235  		modifiedAt := convertTimestampToHumanReadable(
   236  			orgItem.Metadata.ModifiedAt.Seconds,
   237  			orgItem.Metadata.ModifiedAt.Nanos,
   238  		)
   239  		tbl.AddRow(
   240  			orgItem.Metadata.Name,
   241  			orgItem.Metadata.Description,
   242  			orgItem.Metadata.Project,
   243  			orgItem.Metadata.Organization,
   244  			orgItem.Metadata.Partner,
   245  			orgItem.Metadata.ID,
   246  			createAt,
   247  			modifiedAt,
   248  		)
   249  	}
   250  	tbl.Print()
   251  
   252  	if ok := writeContexts(cloudContexts); ok != nil {
   253  		return errors.Wrapf(err, "Failed to write contexts.")
   254  	}
   255  	return nil
   256  }
   257  
   258  func (c *CloudContext) showCurrentContext() error {
   259  	currentContext, err := c.getCurrentContext()
   260  	if err != nil {
   261  		return errors.Wrapf(err, "Failed to get current context.")
   262  	}
   263  
   264  	fmt.Fprintf(c.Out, "Current context: %s\n", currentContext)
   265  	return nil
   266  }
   267  
   268  func (c *CloudContext) showUseContext() error {
   269  	oldContextName, err := c.useContext(c.ContextName)
   270  	if err != nil {
   271  		return errors.Wrapf(err, "Failed to switch context to %s.", c.ContextName)
   272  	}
   273  
   274  	fmt.Fprintf(c.Out, "Successfully switched from %s to context %s.\n", oldContextName, c.ContextName)
   275  	return nil
   276  }
   277  
   278  func (c *CloudContext) showRemoveContext() error {
   279  	if err := c.removeContext(); err != nil {
   280  		return errors.Wrapf(err, "Failed to remove context %s.", c.ContextName)
   281  	}
   282  
   283  	fmt.Fprintf(c.Out, "Context %s removed.\n", c.ContextName)
   284  	return nil
   285  }
   286  
   287  func (c *CloudContext) GetContext() (*CloudContextResponse, error) {
   288  	path := strings.Join([]string{c.APIURL, c.APIPath, "organizations", c.OrgName, "contexts", c.ContextName}, "/")
   289  	response, err := organization.NewRequest(http.MethodGet, path, c.Token, nil)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	var context CloudContextResponse
   295  	err = json.Unmarshal(response, &context)
   296  	if err != nil {
   297  		return nil, errors.Wrapf(err, "Failed to unmarshal context %s.", c.ContextName)
   298  	}
   299  
   300  	return &context, nil
   301  }
   302  
   303  func (c *CloudContext) GetContexts() (*CloudContextsResponse, error) {
   304  	path := strings.Join([]string{c.APIURL, c.APIPath, "organizations", c.OrgName, "contexts"}, "/")
   305  	response, err := organization.NewRequest(http.MethodGet, path, c.Token, nil)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  
   310  	var contexts CloudContextsResponse
   311  	err = json.Unmarshal(response, &contexts)
   312  	if err != nil {
   313  		return nil, errors.Wrap(err, "Failed to unmarshal contexts.")
   314  	}
   315  
   316  	return &contexts, nil
   317  }
   318  
   319  func (c *CloudContext) getCurrentContext() (string, error) {
   320  	currentOrgAndContext, err := organization.GetCurrentOrgAndContext()
   321  	if err != nil {
   322  		return "", errors.Wrap(err, "Failed to get current context.")
   323  	}
   324  
   325  	if ok, err := c.isValidContext(currentOrgAndContext.CurrentContext); !ok {
   326  		return "", err
   327  	}
   328  
   329  	return currentOrgAndContext.CurrentContext, nil
   330  }
   331  
   332  func (c *CloudContext) useContext(contextName string) (string, error) {
   333  	if ok, err := c.isValidContext(contextName); !ok {
   334  		return "", err
   335  	}
   336  
   337  	currentOrgAndContext, err := organization.GetCurrentOrgAndContext()
   338  	if err != nil {
   339  		return "", errors.Wrap(err, "Failed to get current context.")
   340  	}
   341  
   342  	oldContextName := currentOrgAndContext.CurrentContext
   343  	currentOrgAndContext.CurrentContext = contextName
   344  	if err = organization.SetCurrentOrgAndContext(currentOrgAndContext); err != nil {
   345  		return "", errors.Wrapf(err, "Failed to switch context to %s.", contextName)
   346  	}
   347  
   348  	return oldContextName, nil
   349  }
   350  
   351  // RemoveContext TODO: By the way, delete the context stored locally.
   352  func (c *CloudContext) removeContext() error {
   353  	path := strings.Join([]string{c.APIURL, c.APIPath, "organizations", c.OrgName, "contexts", c.ContextName}, "/")
   354  	_, err := organization.NewRequest(http.MethodDelete, path, c.Token, nil)
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	return nil
   360  }
   361  
   362  func (c *CloudContext) isValidContext(contextName string) (bool, error) {
   363  	cloudContexts, err := c.GetContexts()
   364  	if err != nil {
   365  		return false, errors.Wrap(err, "Failed to get contexts.")
   366  	}
   367  
   368  	if cloudContexts == nil || len(cloudContexts.Items) == 0 {
   369  		return false, errors.Wrap(err, "No context found, please create a context on cloud.")
   370  	}
   371  
   372  	for _, item := range cloudContexts.Items {
   373  		if item.Metadata.Name == contextName {
   374  			return true, nil
   375  		}
   376  	}
   377  
   378  	return false, errors.Errorf("Context %s does not exist.", contextName)
   379  }
   380  
   381  func writeContexts(contexts *CloudContextsResponse) error {
   382  	jsonData, err := json.MarshalIndent(contexts, "", "    ")
   383  	if err != nil {
   384  		return err
   385  	}
   386  
   387  	filePath, err := organization.GetContextFilePath()
   388  	if err != nil {
   389  		return err
   390  	}
   391  	file, err := os.Create(filePath)
   392  	if err != nil {
   393  		return err
   394  	}
   395  	defer file.Close()
   396  
   397  	_, err = file.Write(jsonData)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	return nil
   402  }
   403  
   404  func convertTimestampToHumanReadable(seconds int, nanos int) string {
   405  	// Convert seconds and nanoseconds to a time.Time object
   406  	secondsDuration := time.Duration(seconds) * time.Second
   407  	nanosDuration := time.Duration(nanos) * time.Nanosecond
   408  	timestamp := time.Unix(0, int64(secondsDuration+nanosDuration))
   409  
   410  	// Format the time to a human-readable layout
   411  	return timestamp.Format("2006-01-02 15:04:05")
   412  }