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 }