odps/project.go (315 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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. package odps import ( "encoding/xml" "fmt" "io/ioutil" "log" "net/http" "net/url" "strings" "time" "github.com/pkg/errors" "github.com/aliyun/aliyun-odps-go-sdk/odps/common" "github.com/aliyun/aliyun-odps-go-sdk/odps/restclient" "github.com/aliyun/aliyun-odps-go-sdk/odps/security" ) // TODO 将status转换为enum type ProjectStatus int const ( _ = iota ProjectStatusAvailable ProjectStatusReadOnly ProjectStatusDeleting ProjectStatusFrozen ProjectStatusUnKnown ) const ( // ProjectTypeManaged ordinary project ProjectTypeManaged = "managed" // ProjectExternalExternal external project,like hive ProjectExternalExternal = "external" ) type Project struct { model projectModel allProperties []common.Property exists bool beLoaded bool odpsIns *Odps rb common.ResourceBuilder } func (p *Project) OdpsIns() *Odps { return p.odpsIns } type projectModel struct { XMLName xml.Name `xml:"Project"` Name string `xml:"Name"` Type string `xml:"Type"` Comment string `xml:"Comment"` Status ProjectStatus `xml:"State"` ProjectGroupName string `xml:"ProjectGroupName"` Properties []common.Property `xml:"Properties>Property"` DefaultCluster string `xml:"DefaultCluster"` Clusters []Cluster `xml:"Clusters"` ExtendedProperties []common.Property `xml:"ExtendedProperties>Property"` TenantId string `xml:"TenantId"` // 这三个字段在/projects中和/projects/<ProjectName>接口中返回的未知不一样, // 前者是body的xml数据中,后者在header里 Owner string `xml:"Owner"` CreationTime common.GMTTime `xml:"CreationTime"` LastModifiedTime common.GMTTime `xml:"LastModifiedTime"` } type OptionalQuota struct { XMLName xml.Name `xml:"OptionalQuota"` QuotaId string `xml:"QuotaID"` Properties common.Properties `xml:"Properties"` } type Cluster struct { Name string `xml:"Name"` QuotaId string `xml:"QuotaId"` Quotas []OptionalQuota `xml:"Quotas"` } func NewProject(name string, odpsIns *Odps) *Project { return &Project{ model: projectModel{Name: name}, odpsIns: odpsIns, rb: common.ResourceBuilder{ProjectName: name}, } } func (p *Project) RestClient() restclient.RestClient { return p.odpsIns.restClient } type optionalParams struct { // For compatibility. The static class 'Cluster' had strict schema validation. Unmarshalling will // fail because of the new xml tag 'Quotas'. usedByGroupApi bool withAllProperties bool extendedProperties bool } func (p *Project) _loadFromOdps(params optionalParams) (*projectModel, error) { resource := p.rb.Project() client := p.RestClient() urlQuery := make(url.Values) if params.usedByGroupApi { urlQuery.Set("isGroupApi", "true") } if params.withAllProperties { urlQuery.Set("properties", "all") } if params.extendedProperties { urlQuery.Set("extended", "") } model := projectModel{} parseFunc := func(res *http.Response) error { decoder := xml.NewDecoder(res.Body) if err := decoder.Decode(&model); err != nil { return errors.WithStack(err) } header := res.Header model.Owner = header.Get(common.HttpHeaderOdpsOwner) createTimeStr := header.Get(common.HttpHeaderOdpsCreationTime) if createTimeStr != "" { creationTime, err := common.ParseRFC1123Date(createTimeStr) if err != nil { log.Printf("/project get creation time error, %v", err) } model.CreationTime = common.GMTTime(creationTime) } lastModifiedTimeStr := header.Get(common.HttpHeaderLastModified) if lastModifiedTimeStr != "" { lastModifiedTime, err := common.ParseRFC1123Date(lastModifiedTimeStr) if err != nil { log.Printf("/project get last modified time error, %v", err) } model.LastModifiedTime = common.GMTTime(lastModifiedTime) } return nil } if err := client.GetWithParseFunc(resource, urlQuery, nil, parseFunc); err != nil { return nil, errors.WithStack(err) } return &model, nil } // Load should be called before get properties of project func (p *Project) Load() error { model, err := p._loadFromOdps(optionalParams{}) p.beLoaded = true if err != nil { if httpNoteOk, ok := err.(restclient.HttpError); ok { if httpNoteOk.StatusCode == 404 { p.exists = false } } return errors.WithStack(err) } p.exists = true p.model = *model return nil } // IsLoaded whether `Load()` has been called func (p *Project) IsLoaded() bool { return p.beLoaded } func (p *Project) Name() string { return p.model.Name } func (p *Project) Type() string { return p.model.Type } func (p *Project) Comment() string { return p.model.Comment } func (p *Project) Status() ProjectStatus { return p.model.Status } func (p *Project) ProjectGroupName() string { return p.model.ProjectGroupName } // TenantId get project owner's tenant id func (p *Project) TenantId() string { return p.model.TenantId } // PropertiesHasBeSet Properties get the properties those have be set for the project func (p *Project) PropertiesHasBeSet() common.Properties { return p.model.Properties } // GetAllProperties get all the configurable properties of the project, including the // properties inherit from group. // **note**, this method may return error when something wrong during loading data // from the api serer func (p *Project) GetAllProperties() (common.Properties, error) { if p.allProperties != nil { return p.allProperties, nil } model, err := p._loadFromOdps(optionalParams{withAllProperties: true}) if err != nil { return nil, errors.WithStack(err) } p.allProperties = model.Properties return p.allProperties, nil } // GetDefaultCluster Get default cluster. This is an internal method for group-api. // Returns efault cluster when called by group owner, otherwise ,null. // **note**, this method may return error when something wrong during loading data // from the api serer func (p *Project) GetDefaultCluster() (string, error) { if p.model.DefaultCluster != "" { return p.model.DefaultCluster, nil } model, err := p._loadFromOdps(optionalParams{usedByGroupApi: true}) if err != nil { return "", errors.WithStack(err) } p.model.DefaultCluster = model.DefaultCluster p.model.Clusters = model.Clusters return p.model.DefaultCluster, nil } // GetClusters Get information of clusters owned by this project. This is an internal // method for group-api. // **note**, this method may return error when something wrong during loading data // from the api serer func (p *Project) GetClusters() ([]Cluster, error) { if p.model.Clusters != nil { return p.model.Clusters, nil } model, err := p._loadFromOdps(optionalParams{usedByGroupApi: true}) if err != nil { return nil, errors.WithStack(err) } p.model.DefaultCluster = model.DefaultCluster p.model.Clusters = model.Clusters return p.model.Clusters, nil } // GetExtendedProperties get the extended properties of the project // **note**, this method may return error when something wrong during loading data // from the api serer func (p *Project) GetExtendedProperties() (common.Properties, error) { if p.model.ExtendedProperties != nil { return p.model.ExtendedProperties, nil } model, err := p._loadFromOdps(optionalParams{extendedProperties: true}) if err != nil { return nil, errors.WithStack(err) } p.model.ExtendedProperties = model.ExtendedProperties return p.model.ExtendedProperties, nil } func (p *Project) Owner() string { return p.model.Owner } func (p *Project) CreationTime() time.Time { return time.Time(p.model.CreationTime) } func (p *Project) LastModifiedTime() time.Time { return time.Time(p.model.LastModifiedTime) } func (p *Project) Existed() bool { return p.exists } func (p *Project) SecurityManager() security.Manager { return security.NewSecurityManager(p.odpsIns.restClient, p.Name()) } func (p *Project) GetTunnelEndpoint(quotaNames ...string) (string, error) { client := p.odpsIns.restClient resource := p.rb.Tunnel() queryArgs := make(url.Values, 1) queryArgs.Set("service", "") // queryArgs.Set("current_project", p.Name()) if len(quotaNames) > 0 { quotaName := quotaNames[0] if quotaName != "" { queryArgs.Set("quotaName", quotaName) } } req, err := client.NewRequestWithUrlQuery(common.HttpMethod.GetMethod, resource, nil, queryArgs) if err != nil { return "", errors.WithStack(err) } schema := req.URL.Scheme var tunnelEndpoint string err = client.DoWithParseFunc(req, func(res *http.Response) error { // Use ioutil.ReadAll instead of io.ReadAll for compatibility with Go 1.15. b, err := ioutil.ReadAll(res.Body) if err != nil { return errors.WithStack(err) } tunnelEndpoint = string(b) return nil }) return fmt.Sprintf("%s://%s", schema, tunnelEndpoint), errors.WithStack(err) } // Update the project properties, the properties are different in different versioned odps. // When the "properties" is nil, the system will give the project all the default properties. // You'd better ask technique support help when using this method. func (p *Project) Update(properties map[string]string) error { type BodyModel struct { XMLName xml.Name `xml:"Project"` Name string Properties []common.Property `xml:"Properties>Property"` } _properties := make([]common.Property, 0, len(properties)) for key, value := range properties { _properties = append(_properties, common.Property{Name: key, Value: value}) } bodyModel := BodyModel{ Name: p.Name(), Properties: _properties, } resource := p.rb.Project() client := p.RestClient() return client.DoXmlWithModel(common.HttpMethod.PutMethod, resource, nil, &bodyModel, nil) } func (p *Project) Schemas() *Schemas { return NewSchemas(p.odpsIns, p.Name()) } func (p *Project) Tables() *Tables { return NewTables(p.odpsIns, p.Name(), "") } func (status *ProjectStatus) FromStr(s string) { switch strings.ToUpper(s) { case "AVAILABLE": *status = ProjectStatusAvailable case "READONLY": *status = ProjectStatusReadOnly case "DELETING": *status = ProjectStatusDeleting case "FROZEN": *status = ProjectStatusFrozen default: *status = ProjectStatusUnKnown } } func (status ProjectStatus) String() string { switch status { case ProjectStatusAvailable: return "AVAILABLE" case ProjectStatusReadOnly: return "READONLY" case ProjectStatusDeleting: return "DELETING" case ProjectStatusFrozen: return "FROZEN" default: return "UNKNOWN" } } func (status *ProjectStatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return errors.WithStack(err) } status.FromStr(s) return nil } func (status ProjectStatus) MarshalXML(d *xml.Encoder, start xml.StartElement) error { s := status.String() return errors.WithStack(d.EncodeElement(s, start)) }