diff --git a/daemon/json.go b/daemon/json.go index a9d8de0..c0fb242 100644 --- a/daemon/json.go +++ b/daemon/json.go @@ -273,6 +273,15 @@ type CreateCloudClusterJSON struct { Server string `json:"server,omitempty"` } +type CreateColumnarJSON struct { + Timeout string `json:"timeout"` + Environment *store.CloudEnvironment `json:"environment,omitempty"` + Region string `json:"region"` + Provider string `json:"provider"` + EnvName string `json:"env_name"` + Nodes int `json:"nodes"` +} + type AddIPJSON struct { IP string `json:"ip"` } diff --git a/daemon/restserver.go b/daemon/restserver.go index a71c76f..cf9a4ce 100644 --- a/daemon/restserver.go +++ b/daemon/restserver.go @@ -995,6 +995,7 @@ func (d *daemon) HttpCreateCloudCluster(w http.ResponseWriter, r *http.Request) EnvName: reqData.EnvName, Image: reqData.Image, Server: reqData.Server, + IsColumnar: true, } err = d.cloudService.SetupCluster(reqCtx, clusterID, clusterOpts, helper.RestTimeout) @@ -1131,6 +1132,108 @@ func (d *daemon) HttpGetClusterCertificate(w http.ResponseWriter, r *http.Reques writeJsonResponse(w, c) } +func (d *daemon) HttpCreateCloudColumnar(w http.ResponseWriter, r *http.Request) { + reqCtx, err := getHttpContext(r) + if err != nil { + writeJSONError(w, err) + return + } + + var reqData CreateColumnarJSON + err = readJsonRequest(r, &reqData) + if err != nil { + writeJSONError(w, err) + return + } + + timeout := 1 * time.Hour + + if reqData.Timeout != "" { + clusterTimeout, err := time.ParseDuration(reqData.Timeout) + if err != nil { + writeJSONError(w, err) + return + } + + timeout = clusterTimeout + } + + if timeout < 0 { + writeJSONError(w, errors.New("must specify a valid timeout for the cluster")) + return + } + if timeout > 2*7*24*time.Hour { + writeJSONError(w, errors.New("cannot allocate clusters for longer than 2 weeks")) + return + } + + clusterID := helper.NewRandomClusterID() + + meta := store.ClusterMeta{ + Owner: dyncontext.ContextUser(reqCtx), + Timeout: time.Now().Add(timeout), + Platform: store.ClusterPlatformCloud, + UseSecure: true, + CloudEnvironment: reqData.Environment, + CloudEnvName: reqData.EnvName, + } + if err := d.metaStore.CreateClusterMeta(clusterID, meta); err != nil { + writeJSONError(w, err) + return + } + + opts := cloud.CreateColumnarOptions{ + Timeout: reqData.Timeout, + Environment: reqData.Environment, + Region: reqData.Region, + Provider: reqData.Provider, + EnvName: reqData.EnvName, + Nodes: reqData.Nodes, + } + + err = d.cloudService.SetupColumnar(reqCtx, clusterID, opts, helper.RestTimeout) + if err != nil { + writeJSONError(w, err) + return + } + + // update timeout to account for the time it takes to provision the cluster + err = d.metaStore.UpdateClusterMeta(clusterID, func(meta store.ClusterMeta) (store.ClusterMeta, error) { + meta.Timeout = time.Now().Add(timeout) + return meta, nil + }) + + dCtx, cancel := context.WithDeadline(reqCtx, time.Now().Add(helper.RestTimeout)) + defer cancel() + + c, err := d.cloudService.GetColumnar(dCtx, clusterID) + if err != nil { + writeJSONError(w, err) + return + } + + newClusterJson := jsonifyCluster(c) + writeJsonResponse(w, newClusterJson) +} + +func (d *daemon) HttpCreateCloudColumnarAPIKeys(w http.ResponseWriter, r *http.Request) { + reqCtx, err := getHttpContext(r) + if err != nil { + writeJSONError(w, err) + return + } + + clusterID := mux.Vars(r)["cluster_id"] + + key, err := d.cloudService.CreateColumnarKey(reqCtx, clusterID) + if err != nil { + writeJSONError(w, err) + return + } + + writeJsonResponse(w, key) +} + func (d *daemon) createRESTRouter() *mux.Router { r := mux.NewRouter() r.HandleFunc("/", d.HttpRoot) @@ -1139,6 +1242,8 @@ func (d *daemon) createRESTRouter() *mux.Router { r.HandleFunc("/clusters", d.HttpGetClusters).Methods("GET") r.HandleFunc("/clusters", d.HttpCreateCluster).Methods("POST") r.HandleFunc("/create-cloud", d.HttpCreateCloudCluster).Methods("POST") + r.HandleFunc("/create-columnar", d.HttpCreateCloudColumnar).Methods("POST") + r.HandleFunc("/columnar/{cluster_id}/create-key", d.HttpCreateCloudColumnarAPIKeys).Methods("POST") r.HandleFunc("/cluster/{cluster_id}", d.HttpGetCluster).Methods("GET") r.HandleFunc("/cluster/{cluster_id}", d.HttpUpdateCluster).Methods("PUT") r.HandleFunc("/cluster/{cluster_id}/setup", d.HttpSetupCluster).Methods("POST") diff --git a/service/cloud/cloudservice.go b/service/cloud/cloudservice.go index 08caa33..92c27ec 100644 --- a/service/cloud/cloudservice.go +++ b/service/cloud/cloudservice.go @@ -9,6 +9,7 @@ import ( "log" "math/rand" "regexp" + "strconv" "strings" "time" @@ -116,6 +117,34 @@ func (cs *CloudService) getCluster(ctx context.Context, cloudClusterID string, e return &respBody, nil } +func (cs *CloudService) getColumnar(ctx context.Context, cloudClusterID string, env *store.CloudEnvironment) (*getColumnarJSON, error) { + res, err := cs.client.DoInternal(ctx, "GET", fmt.Sprintf(createColumnarPath+"/%s", env.TenantID, env.ProjectID, cloudClusterID), nil, env) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("get columnar failed: reason could not be determined: %v", err) + } + return nil, fmt.Errorf("get columnar failed: %s", string(bb)) + } + + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("get columnar succeeded: but body could not be read: %v", err) + } + + var respBody getColumnarJSON + if err := json.Unmarshal(bb, &respBody); err != nil { + return nil, err + } + + return &respBody, nil +} + func (cs *CloudService) addBucket(ctx context.Context, clusterID, cloudClusterID string, opts service.AddBucketOptions, env *store.CloudEnvironment) error { log.Printf("Running cloud CreateBucket for %s: %s", clusterID, cloudClusterID) @@ -183,6 +212,37 @@ func (cs *CloudService) addIP(ctx context.Context, clusterID, cloudClusterID, ip return nil } +func (cs *CloudService) addColumnarIP(ctx context.Context, clusterID, cloudClusterID, ip string, env *store.CloudEnvironment) error { + log.Printf("Running cloud AddIP for %s: %s", clusterID, cloudClusterID) + + body := allowListJSON{ + CIDR: ip, + Comment: "Any IP", + } + + res, err := cs.client.DoInternal(ctx, "POST", fmt.Sprintf(addColumnarIPPath, env.TenantID, env.ProjectID, cloudClusterID), body, env) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("add ip failed: reason could not be determined: %v", err) + } + errorBody := string(bb) + // AV-35851: Cluster randomly goes into deploying state after adding IP + if strings.Contains(errorBody, "ErrClusterStateNotNormal") { + time.Sleep(time.Second * 5) + return cs.addIP(ctx, clusterID, cloudClusterID, ip, env) + } + return fmt.Errorf("add ip failed: %s", string(bb)) + } + + return nil +} + func (cs *CloudService) killCluster(ctx context.Context, clusterID, cloudClusterID string, env *store.CloudEnvironment) error { log.Printf("Running cloud KillCluster for %s: %s", clusterID, cloudClusterID) @@ -205,6 +265,29 @@ func (cs *CloudService) killCluster(ctx context.Context, clusterID, cloudCluster return nil } +func (cs *CloudService) killColumnar(ctx context.Context, clusterID, cloudClusterID string, env *store.CloudEnvironment) error { + log.Printf("Running cloud KillColumnar for %s: %s", clusterID, cloudClusterID) + + path := fmt.Sprintf("/v2/organizations/%s/projects/%s/instance/%s", env.TenantID, env.ProjectID, cloudClusterID) + res, err := cs.client.DoInternal(ctx, "DELETE", path, nil, env) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("failed to kill columnar cluster: %s: %s", clusterID, cloudClusterID) + return fmt.Errorf("kill columnar cluster failed: reason could not be determined: %v", err) + } + log.Printf("failed to kill columnar: %s: %s: %s", clusterID, cloudClusterID, string(bb)) + return fmt.Errorf("kill columnar failed: %s", string(bb)) + } + + return nil +} + func (cs *CloudService) addUser(ctx context.Context, clusterID, cloudClusterID string, user *helper.UserOption, env *store.CloudEnvironment) error { log.Printf("Running cloud AddUser for %s: %s", clusterID, cloudClusterID) @@ -350,6 +433,52 @@ func (cs *CloudService) GetCluster(ctx context.Context, clusterID string) (*clus }, nil } +func (cs *CloudService) GetColumnar(ctx context.Context, clusterID string) (*cluster.Cluster, error) { + if !cs.enabled { + return nil, ErrCloudNotEnabled + } + + _, env, err := cs.getCloudClusterEnv(ctx, clusterID) + if err != nil { + return nil, err + } + + meta, err := cs.metaStore.GetClusterMeta(clusterID) + if err != nil { + log.Printf("Encountered unregistered cluster: %s", clusterID) + return nil, err + } + + if meta.CloudClusterID == "" { + log.Printf("Encountered columnar with no cloud cluster ID: %s", clusterID) + return nil, errors.New("unknown cluster") + } + + log.Printf("Running cloud GetColumnar for %s: %s", clusterID, meta.CloudClusterID) + + c, err := cs.getColumnar(ctx, meta.CloudClusterID, env) + if err != nil { + return nil, err + } + + var nodes []*cluster.Node + nodes = append(nodes, &cluster.Node{ + ContainerID: c.Data.Id, + Name: c.Data.Name, + InitialServerVersion: strconv.Itoa(c.Data.Version), + }) + + return &cluster.Cluster{ + ID: clusterID, + Creator: meta.Owner, + Owner: meta.Owner, + Timeout: meta.Timeout, + Nodes: nodes, + EntryPoint: c.Data.Config.Endpoint, + Status: c.Data.State, + }, nil +} + func (cs *CloudService) AddUser(ctx context.Context, clusterID string, opts service.AddUserOptions, connCtx service.ConnectContext) error { if !cs.enabled { return ErrCloudNotEnabled @@ -380,7 +509,60 @@ func (cs *CloudService) AddIP(ctx context.Context, clusterID, ip string) error { func (cs *CloudService) getAllClusters(ctx context.Context, env *store.CloudEnvironment) ([]*cluster.Cluster, error) { // TODO: Implement pagination // TODO: Support listing get all clusters across custom environments - res, err := cs.client.Do(ctx, "GET", getAllClustersPath+fmt.Sprintf("?perPage=1000&projectId=%s", env.ProjectID), nil, env) + //res, err := cs.client.Do(ctx, "GET", getAllClustersPath+fmt.Sprintf("?perPage=1000&projectId=%s", env.ProjectID), nil, env) + res, err := cs.client.DoInternal(ctx, "GET", fmt.Sprintf("/v2/organizations/%s/clusters?page=1&perPage=1000&projectId=%s", env.TenantID, env.ProjectID), nil, env) + + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("get all clusters failed: reason could not be determined: %v", err) + } + return nil, fmt.Errorf("get all clusters failed: %s", string(bb)) + } + + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("get all clusters succeeded: but body could not be read: %v", err) + } + + //var respBody getAllClustersJSON + var respBody getAllClustersInternalJSON + if err := json.Unmarshal(bb, &respBody); err != nil { + return nil, err + } + + var clusters []*cluster.Cluster + for _, d := range respBody.Data { + if d.Items.Project.ID != env.ProjectID { + continue + } + c, err := cs.GetCluster(ctx, d.Items.Name) + if err != nil { + log.Printf("Failed to get cluster: %s: %v", d.Items.Name, err) + continue + } + + if !dyncontext.ContextIgnoreOwnership(ctx) && c.Owner != dyncontext.ContextUser(ctx) { + continue + } + + clusters = append(clusters, c) + } + + return clusters, nil +} + +func (cs *CloudService) getAllColumnars(ctx context.Context, env *store.CloudEnvironment) ([]*cluster.Cluster, error) { + // TODO: Implement pagination + // TODO: Support listing get all clusters across custom environments + //res, err := cs.client.Do(ctx, "GET", getAllClustersPath+fmt.Sprintf("?perPage=1000&projectId=%s", env.ProjectID), nil, env) + res, err := cs.client.DoInternal(ctx, "GET", fmt.Sprintf("/v2/organizations/%s/instance?page=1&perPage=1000&projectId=%s", env.TenantID, env.ProjectID), nil, env) + if err != nil { return nil, err } @@ -399,16 +581,19 @@ func (cs *CloudService) getAllClusters(ctx context.Context, env *store.CloudEnvi return nil, fmt.Errorf("get all clusters succeeded: but body could not be read: %v", err) } - var respBody getAllClustersJSON + var respBody getAllColumnarInternalJSON if err := json.Unmarshal(bb, &respBody); err != nil { return nil, err } var clusters []*cluster.Cluster - for _, d := range respBody.Data.Items { - c, err := cs.GetCluster(ctx, d.Name) + for _, d := range respBody.Items { + if d.Data.ProjectID != env.ProjectID { + continue + } + c, err := cs.GetColumnar(ctx, d.Data.Name) if err != nil { - log.Printf("Failed to get cluster: %s: %v", d.Name, err) + log.Printf("Failed to get cluster: %s: %v", d.Data.Name, err) continue } @@ -437,8 +622,10 @@ func (cs *CloudService) GetAllClusters(ctx context.Context) ([]*cluster.Cluster, if err != nil { return nil, err } - allClusters = append(allClusters, clusters...) + + columnars, err := cs.getAllColumnars(ctx, env) + allClusters = append(columnars, clusters...) } return allClusters, nil @@ -454,6 +641,15 @@ func (cs *CloudService) KillCluster(ctx context.Context, clusterID string) error return err } + meta, err := cs.metaStore.GetClusterMeta(clusterID) + if err != nil { + log.Printf("Encountered unregistered cluster: %s", clusterID) + return err + } + + if meta.Columnar { + return cs.killColumnar(ctx, clusterID, cloudClusterID, env) + } return cs.killCluster(ctx, clusterID, cloudClusterID, env) } @@ -553,6 +749,128 @@ func (cs *CloudService) postClusterCreate(ctx context.Context, clusterID, cloudC return nil } +func (cs *CloudService) SetupColumnar(ctx context.Context, clusterID string, opts CreateColumnarOptions, + maxRequestTimeout time.Duration) error { + env := cs.defaultEnv + + if opts.EnvName != "" { + customEnv, ok := cs.envs[opts.EnvName] + if !ok { + return fmt.Errorf("environment %s not found", opts.EnvName) + } + env = customEnv + } + + if opts.Environment != nil { + env = opts.Environment + } + + provider := defaultProvider + if opts.Provider != "" { + switch opts.Provider { + case "aws": + provider = ProviderHostedAWS + case "gcp": + provider = ProviderHostedGCP + default: + return fmt.Errorf("provider %s is not supported", opts.Provider) + } + } + + region := opts.Region + if region == "" { + switch provider { + case ProviderHostedAWS: + region = defaultRegionAWS + case ProviderHostedGCP: + region = defaultRegionGCP + } + } + + reqCtx, cancel := context.WithDeadline(ctx, time.Now().Add(maxRequestTimeout)) + defer cancel() + + columnarBody := CreateColumnarInstance{ + Name: clusterID, + Provider: provider, + Region: region, + Nodes: opts.Nodes, + } + res, err := cs.client.DoInternal(reqCtx, "POST", fmt.Sprintf(createColumnarPath, env.TenantID, env.ProjectID), columnarBody, env) + if err != nil { + return err + } + defer res.Body.Close() + + type ID struct { + ID string `json:"id"` + } + + b, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + id := &ID{} + err = json.Unmarshal(b, id) + if err != nil { + return err + } + + cloudClusterID := id.ID + + err = cs.metaStore.UpdateClusterMeta(clusterID, func(meta store.ClusterMeta) (store.ClusterMeta, error) { + meta.Columnar = true + meta.CloudClusterID = cloudClusterID + return meta, nil + }) + if err != nil { + return err + } + + err = cs.postColumnarCreate(ctx, clusterID, cloudClusterID, env, maxRequestTimeout) + if err != nil { + go func() { + cs.killColumnar(ctx, clusterID, cloudClusterID, env) + }() + return err + } + + return nil + +} + +func (cs *CloudService) postColumnarCreate(ctx context.Context, clusterID, cloudClusterID string, env *store.CloudEnvironment, maxRequestTimeout time.Duration) error { + tCtx, cancel := context.WithDeadline(ctx, time.Now().Add(25*time.Minute)) + defer cancel() + + for { + getReqCtx, cancel := context.WithDeadline(tCtx, time.Now().Add(maxRequestTimeout)) + + // If the tCtx deadline expires then this return an error. + c, err := cs.getColumnar(getReqCtx, cloudClusterID, env) + cancel() + if err != nil { + return err + } + + if c.Data.State == clusterHealthy { + break + } + + if c.Data.State == clusterDeploymentFailed { + return errors.New("create cluster failed: status is deploymentFailed") + } + + time.Sleep(5 * time.Second) + } + // allow all ips, these are only temporary, non security sensitive clusters so it's fine + if err := cs.addColumnarIP(ctx, clusterID, cloudClusterID, "0.0.0.0/0", env); err != nil { + return err + } + + return nil +} + func (cs *CloudService) SetupCluster(ctx context.Context, clusterID string, opts ClusterSetupOptions, maxRequestTimeout time.Duration) error { if !cs.enabled { @@ -811,6 +1129,19 @@ func (cs *CloudService) ConnString(ctx context.Context, clusterID string, useSSL return "", errors.New("only SSL supported for cloud") } + meta, err := cs.metaStore.GetClusterMeta(clusterID) + if err != nil { + log.Printf("Encountered unregistered cluster: %s", clusterID) + return "", err + } + if meta.Columnar { + c, err := cs.GetColumnar(ctx, clusterID) + if err != nil { + return "", err + } + return "couchbases://" + c.EntryPoint, nil + } + c, err := cs.GetCluster(ctx, clusterID) if err != nil { return "", err @@ -879,3 +1210,38 @@ func (cs *CloudService) GetCertificate(ctx context.Context, clusterID string) (s return strings.TrimSpace(lastCert.Pem), nil } + +func (cs *CloudService) CreateColumnarKey(ctx context.Context, clusterID string) (*ColumnarApiKeys, error) { + return cs.createColumnarKey(ctx, clusterID) +} + +func (cs *CloudService) createColumnarKey(ctx context.Context, clusterID string) (*ColumnarApiKeys, error) { + cloudClusterId, env, err := cs.getCloudClusterEnv(ctx, clusterID) + if err != nil { + return nil, err + } + + path := fmt.Sprintf("/v2/organizations/%s/projects/%s/instance/%s/apikeys", env.TenantID, env.ProjectID, cloudClusterId) + res, err := cs.client.doInternal(ctx, "POST", path, nil, false, env) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + bb, err := ioutil.ReadAll(res.Body) + return nil, fmt.Errorf("unable to create api keys: %s, %v", string(bb), err) + } + + bb, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("create api keys succeeded: but body could not be read: %v", err) + } + + respBody := &ColumnarApiKeys{} + if err := json.Unmarshal(bb, &respBody); err != nil { + return nil, err + } + + return respBody, nil +} diff --git a/service/cloud/constants.go b/service/cloud/constants.go index b4c0703..2ead6d3 100644 --- a/service/cloud/constants.go +++ b/service/cloud/constants.go @@ -20,6 +20,11 @@ const ( sessionsPath = "/sessions" createUserPath = internalBasePath + "/users" + createColumnarPath = "/v2/organizations/%s/projects/%s/instance" + internalInstanceBasePath = "/v2/organizations/%s/projects/%s/instance/%s" + + addColumnarIPPath = internalInstanceBasePath + "/allowlists" + clusterHealthy = "healthy" clusterDeleting = "destroying" clusterDeploying = "deploying" diff --git a/service/cloud/json.go b/service/cloud/json.go index dd12bda..7efe38d 100644 --- a/service/cloud/json.go +++ b/service/cloud/json.go @@ -1,8 +1,11 @@ package cloud type getAllClustersClusterJSON struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id"` + Name string `json:"name"` + Project struct { + ID string `json:"id"` + } `json:"project"` } type getAllClustersJSON struct { @@ -11,6 +14,16 @@ type getAllClustersJSON struct { } `json:"data"` } +type getAllClustersInternalJSON struct { + Data []struct { + Items getAllClustersClusterJSON `json:"data"` + } `json:"data"` +} + +type getAllColumnarInternalJSON struct { + Items []getColumnarJSON `json:"data"` +} + type getClusterJSONVersion struct { Name string `json:"name"` } @@ -249,3 +262,38 @@ type GetTrustedCAsResponse_Certificate struct { NotAfter string `json:"notAfter"` Pem string `json:"pem"` } + +type CreateColumnarInstance struct { + Name string `json:"name"` + Description string `json:"description"` + Provider Provider `json:"provider"` + Region string `json:"region"` + Nodes int `json:"nodes"` +} + +type getColumnarJSON struct { + Data columnarData `json:"data"` +} + +type columnarData struct { + Id string `json:"id"` + TenantId string `json:"tenantId"` + ProjectID string `json:"projectId"` + State string `json:"state"` + Name string `json:"name"` + Version int `json:"version"` + Config columnarConfig `json:"config"` +} + +type columnarConfig struct { + Provider string `json:"provider"` + Region string `json:"region"` + NodeCount int `json:"nodeCount"` + Endpoint string `json:"endpoint"` + ClusterId string `json:"clusterId"` +} + +type ColumnarApiKeys struct { + APIKeyId string `json:apikeyId` + Secret string `json:secret` +} diff --git a/service/cloud/options.go b/service/cloud/options.go index ee62001..559ea21 100644 --- a/service/cloud/options.go +++ b/service/cloud/options.go @@ -18,4 +18,14 @@ type ClusterSetupOptions struct { EnvName string Image string Server string + IsColumnar bool +} + +type CreateColumnarOptions struct { + Timeout string + Environment *store.CloudEnvironment + Region string + Provider string + EnvName string + Nodes int } diff --git a/store/metadatastore.go b/store/metadatastore.go index 93278cd..84a615d 100644 --- a/store/metadatastore.go +++ b/store/metadatastore.go @@ -55,6 +55,7 @@ type ClusterMetaJSON struct { OS string `json:"os,omitempty"` CloudEnvironment *CloudEnvironment `json:"cloudEnvironment,omitempty"` CloudEnvName string `json:"cloudEnvName,omitempty"` + Columnar bool `json:"columnar,omitempty"` } type ClusterMeta struct { @@ -66,6 +67,7 @@ type ClusterMeta struct { OS string CloudEnvironment *CloudEnvironment CloudEnvName string + Columnar bool } type MetaDataStore struct { @@ -96,6 +98,7 @@ func (store *MetaDataStore) serializeMeta(meta ClusterMeta) ([]byte, error) { OS: meta.OS, CloudEnvironment: meta.CloudEnvironment, CloudEnvName: meta.CloudEnvName, + Columnar: meta.Columnar, } metaBytes, err := json.Marshal(metaJSON) @@ -127,6 +130,7 @@ func (store *MetaDataStore) deserializeMeta(bytes []byte) (ClusterMeta, error) { OS: metaJSON.OS, CloudEnvironment: metaJSON.CloudEnvironment, CloudEnvName: metaJSON.CloudEnvName, + Columnar: metaJSON.Columnar, }, nil }