godeps: update vmware/govmomi

Update required to continue work on #64021

- The govmomi tag API changed

- Pulling in the new vapi/simulator package for testing the VCP Zones impl
This commit is contained in:
Doug MacEachern
2018-08-22 11:11:11 -07:00
parent c0be4cc09c
commit 5816a8bc18
21 changed files with 1853 additions and 930 deletions

View File

@@ -37,6 +37,9 @@ filegroup(
"//vendor/github.com/vmware/govmomi/simulator:all-srcs",
"//vendor/github.com/vmware/govmomi/sts:all-srcs",
"//vendor/github.com/vmware/govmomi/task:all-srcs",
"//vendor/github.com/vmware/govmomi/vapi/internal:all-srcs",
"//vendor/github.com/vmware/govmomi/vapi/rest:all-srcs",
"//vendor/github.com/vmware/govmomi/vapi/simulator:all-srcs",
"//vendor/github.com/vmware/govmomi/vapi/tags:all-srcs",
"//vendor/github.com/vmware/govmomi/vim25:all-srcs",
],

View File

@@ -83,4 +83,4 @@ Refer to the [CHANGELOG](CHANGELOG.md) for version to version changes.
## License
govmomi is available under the [Apache 2 license](LICENSE).
govmomi is available under the [Apache 2 license](LICENSE.txt).

View File

@@ -137,8 +137,12 @@ func (s *SessionManager) LoginByToken(ctx *Context, req *types.LoginByToken) soa
func (s *SessionManager) Logout(ctx *Context, _ *types.Logout) soap.HasFault {
session := ctx.Session
delete(s.sessions, session.Key)
pc := Map.content().PropertyCollector
for ref, obj := range ctx.Session.Registry.objects {
if ref == pc {
continue // don't unregister the PropertyCollector singleton
}
if _, ok := obj.(RegisterObject); ok {
ctx.Map.Remove(ref) // Remove RegisterObject handlers
}

27
vendor/github.com/vmware/govmomi/vapi/internal/BUILD generated vendored Normal file
View File

@@ -0,0 +1,27 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["internal.go"],
importmap = "k8s.io/kubernetes/vendor/github.com/vmware/govmomi/vapi/internal",
importpath = "github.com/vmware/govmomi/vapi/internal",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/vmware/govmomi/vim25/mo:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25/types:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,126 @@
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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.
*/
package internal
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)
const (
Path = "/rest/com/vmware"
SessionPath = "/cis/session"
CategoryPath = "/cis/tagging/category"
TagPath = "/cis/tagging/tag"
AssociationPath = "/cis/tagging/tag-association"
SessionCookieName = "vmware-api-session-id"
)
// AssociatedObject is the same structure as types.ManagedObjectReference,
// just with a different field name (ID instead of Value).
// In the API we use mo.Reference, this type is only used for wire transfer.
type AssociatedObject struct {
Type string `json:"type"`
Value string `json:"id"`
}
// Reference implements mo.Reference
func (o AssociatedObject) Reference() types.ManagedObjectReference {
return types.ManagedObjectReference(o)
}
// Association for tag-association requests.
type Association struct {
TagID string `json:"tag_id,omitempty"`
ObjectID *AssociatedObject `json:"object_id,omitempty"`
}
// NewAssociation returns an Association, converting ref to an AssociatedObject.
func NewAssociation(tagID string, ref mo.Reference) Association {
obj := AssociatedObject(ref.Reference())
return Association{
TagID: tagID,
ObjectID: &obj,
}
}
type CloneURL interface {
URL() *url.URL
}
// Resource wraps url.URL with helpers
type Resource struct {
u *url.URL
}
func URL(c CloneURL, path string) *Resource {
r := &Resource{u: c.URL()}
r.u.Path = Path + path
return r
}
// WithID appends id to the URL.Path
func (r *Resource) WithID(id string) *Resource {
r.u.Path += "/id:" + id
return r
}
// WithAction sets adds action to the URL.RawQuery
func (r *Resource) WithAction(action string) *Resource {
r.u.RawQuery = url.Values{
"~action": []string{action},
}.Encode()
return r
}
// Request returns a new http.Request for the given method.
// An optional body can be provided for POST and PATCH methods.
func (r *Resource) Request(method string, body ...interface{}) *http.Request {
rdr := io.MultiReader() // empty body by default
if len(body) != 0 {
rdr = encode(body[0])
}
req, err := http.NewRequest(method, r.u.String(), rdr)
if err != nil {
panic(err)
}
return req
}
type errorReader struct {
e error
}
func (e errorReader) Read([]byte) (int, error) {
return -1, e.e
}
// encode body as JSON, deferring any errors until io.Reader is used.
func encode(body interface{}) io.Reader {
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(body)
if err != nil {
return errorReader{err}
}
return &b
}

28
vendor/github.com/vmware/govmomi/vapi/rest/BUILD generated vendored Normal file
View File

@@ -0,0 +1,28 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["client.go"],
importmap = "k8s.io/kubernetes/vendor/github.com/vmware/govmomi/vapi/rest",
importpath = "github.com/vmware/govmomi/vapi/rest",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/vmware/govmomi/vapi/internal:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25/soap:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

105
vendor/github.com/vmware/govmomi/vapi/rest/client.go generated vendored Normal file
View File

@@ -0,0 +1,105 @@
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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.
*/
package rest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/vmware/govmomi/vapi/internal"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
)
// Client extends soap.Client to support JSON encoding, while inheriting security features, debug tracing and session persistence.
type Client struct {
*soap.Client
}
// NewClient creates a new Client instance.
func NewClient(c *vim25.Client) *Client {
sc := c.Client.NewServiceClient(internal.Path, "")
return &Client{sc}
}
// Do sends the http.Request, decoding resBody if provided.
func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{}) error {
switch req.Method {
case http.MethodPost, http.MethodPatch:
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
return c.Client.Do(ctx, req, func(res *http.Response) error {
switch res.StatusCode {
case http.StatusOK:
case http.StatusBadRequest:
// TODO: structured error types
detail, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail))
default:
return fmt.Errorf("%s %s: %s", req.Method, req.URL, res.Status)
}
if resBody == nil {
return nil
}
switch b := resBody.(type) {
case io.Writer:
_, err := io.Copy(b, res.Body)
return err
default:
val := struct {
Value interface{} `json:"value,omitempty"`
}{
resBody,
}
return json.NewDecoder(res.Body).Decode(&val)
}
})
}
// Login creates a new session via Basic Authentication with the given url.Userinfo.
func (c *Client) Login(ctx context.Context, user *url.Userinfo) error {
req := internal.URL(c, internal.SessionPath).Request(http.MethodPost)
if user != nil {
if password, ok := user.Password(); ok {
req.SetBasicAuth(user.Username(), password)
}
}
return c.Do(ctx, req, nil)
}
// Logout deletes the current session.
func (c *Client) Logout(ctx context.Context) error {
req := internal.URL(c, internal.SessionPath).Request(http.MethodDelete)
return c.Do(ctx, req, nil)
}

29
vendor/github.com/vmware/govmomi/vapi/simulator/BUILD generated vendored Normal file
View File

@@ -0,0 +1,29 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["simulator.go"],
importmap = "k8s.io/kubernetes/vendor/github.com/vmware/govmomi/vapi/simulator",
importpath = "github.com/vmware/govmomi/vapi/simulator",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/google/uuid:go_default_library",
"//vendor/github.com/vmware/govmomi/vapi/internal:go_default_library",
"//vendor/github.com/vmware/govmomi/vapi/tags:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25/types:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,354 @@
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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.
*/
package simulator
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"path"
"reflect"
"strings"
"sync"
"github.com/google/uuid"
"github.com/vmware/govmomi/vapi/internal"
"github.com/vmware/govmomi/vapi/tags"
vim "github.com/vmware/govmomi/vim25/types"
)
type handler struct {
*http.ServeMux
sync.Mutex
Category map[string]*tags.Category
Tag map[string]*tags.Tag
Association map[string]map[internal.AssociatedObject]bool
}
// New creates a vAPI simulator.
func New(u *url.URL, settings []vim.BaseOptionValue) (string, http.Handler) {
s := &handler{
ServeMux: http.NewServeMux(),
Category: make(map[string]*tags.Category),
Tag: make(map[string]*tags.Tag),
Association: make(map[string]map[internal.AssociatedObject]bool),
}
handlers := []struct {
p string
m http.HandlerFunc
}{
{internal.SessionPath, s.session},
{internal.CategoryPath, s.category},
{internal.CategoryPath + "/", s.categoryID},
{internal.TagPath, s.tag},
{internal.TagPath + "/", s.tagID},
{internal.AssociationPath, s.association},
}
for i := range handlers {
h := handlers[i]
s.HandleFunc(internal.Path+h.p, func(w http.ResponseWriter, r *http.Request) {
s.Lock()
defer s.Unlock()
h.m(w, r)
})
}
return internal.Path + "/", s
}
// ok responds with http.StatusOK and json encodes val if given.
func (s *handler) ok(w http.ResponseWriter, val ...interface{}) {
w.WriteHeader(http.StatusOK)
if len(val) == 0 {
return
}
err := json.NewEncoder(w).Encode(struct {
Value interface{} `json:"value,omitempty"`
}{
val[0],
})
if err != nil {
log.Panic(err)
}
}
func (s *handler) fail(w http.ResponseWriter, kind string) {
w.WriteHeader(http.StatusBadRequest)
err := json.NewEncoder(w).Encode(struct {
Type string `json:"type"`
Value struct {
Messages []string `json:"messages,omitempty"`
} `json:"value,omitempty"`
}{
Type: kind,
})
if err != nil {
log.Panic(err)
}
}
// ServeHTTP handles vAPI requests.
func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost, http.MethodDelete, http.MethodGet, http.MethodPatch:
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
h, _ := s.Handler(r)
h.ServeHTTP(w, r)
}
func (s *handler) decode(r *http.Request, w http.ResponseWriter, val interface{}) bool {
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(val)
if err != nil {
log.Printf("%s %s: %s", r.Method, r.RequestURI, err)
w.WriteHeader(http.StatusBadRequest)
return false
}
return true
}
func (s *handler) session(w http.ResponseWriter, r *http.Request) {
var id string
switch r.Method {
case http.MethodPost:
id = uuid.New().String()
// TODO: save session
http.SetCookie(w, &http.Cookie{
Name: internal.SessionCookieName,
Value: id,
})
s.ok(w)
case http.MethodDelete:
// TODO: delete session
s.ok(w)
case http.MethodGet:
// TODO: test is session is valid
s.ok(w, id)
}
}
func (s *handler) action(r *http.Request) string {
return r.URL.Query().Get("~action")
}
func (s *handler) id(r *http.Request) string {
id := path.Base(r.URL.Path)
return strings.TrimPrefix(id, "id:")
}
func newID(kind string) string {
return fmt.Sprintf("urn:vmomi:InventoryService%s:%s:GLOBAL", kind, uuid.New().String())
}
func (s *handler) category(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var spec struct {
Category tags.Category `json:"create_spec"`
}
if s.decode(r, w, &spec) {
for _, category := range s.Category {
if category.Name == spec.Category.Name {
s.fail(w, "com.vmware.vapi.std.errors.already_exists")
return
}
}
id := newID("Category")
spec.Category.ID = id
s.Category[id] = &spec.Category
s.ok(w, id)
}
case http.MethodGet:
var ids []string
for id := range s.Category {
ids = append(ids, id)
}
s.ok(w, ids)
}
}
func (s *handler) categoryID(w http.ResponseWriter, r *http.Request) {
id := s.id(r)
o, ok := s.Category[id]
if !ok {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodDelete:
delete(s.Category, id)
for ix, tag := range s.Tag {
if tag.CategoryID == id {
delete(s.Tag, ix)
delete(s.Association, ix)
}
}
s.ok(w)
case http.MethodPatch:
var spec struct {
Category tags.Category `json:"update_spec"`
}
if s.decode(r, w, &spec) {
ntypes := len(spec.Category.AssociableTypes)
if ntypes != 0 {
// Validate that AssociableTypes is only appended to.
etypes := len(o.AssociableTypes)
fail := ntypes < etypes
if !fail {
fail = !reflect.DeepEqual(o.AssociableTypes, spec.Category.AssociableTypes[:etypes])
}
if fail {
s.fail(w, "com.vmware.vapi.std.errors.invalid_argument")
return
}
}
o.Patch(&spec.Category)
s.ok(w)
}
case http.MethodGet:
s.ok(w, o)
}
}
func (s *handler) tag(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var spec struct {
Tag tags.Tag `json:"create_spec"`
}
if s.decode(r, w, &spec) {
for _, tag := range s.Tag {
if tag.Name == spec.Tag.Name {
s.fail(w, "com.vmware.vapi.std.errors.already_exists")
return
}
}
id := newID("Tag")
spec.Tag.ID = id
s.Tag[id] = &spec.Tag
s.Association[id] = make(map[internal.AssociatedObject]bool)
s.ok(w, id)
}
case http.MethodGet:
var ids []string
for id := range s.Tag {
ids = append(ids, id)
}
s.ok(w, ids)
}
}
func (s *handler) tagID(w http.ResponseWriter, r *http.Request) {
id := s.id(r)
switch s.action(r) {
case "list-tags-for-category":
var ids []string
for _, tag := range s.Tag {
if tag.CategoryID == id {
ids = append(ids, tag.ID)
}
}
s.ok(w, ids)
return
}
o, ok := s.Tag[id]
if !ok {
log.Printf("tag not found: %s", id)
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodDelete:
delete(s.Tag, id)
delete(s.Association, id)
s.ok(w)
case http.MethodPatch:
var spec struct {
Tag tags.Tag `json:"update_spec"`
}
if s.decode(r, w, &spec) {
o.Patch(&spec.Tag)
s.ok(w)
}
case http.MethodGet:
s.ok(w, o)
}
}
func (s *handler) association(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var spec internal.Association
if !s.decode(r, w, &spec) {
return
}
if spec.TagID != "" {
if _, exists := s.Association[spec.TagID]; !exists {
log.Printf("association tag not found: %s", spec.TagID)
http.NotFound(w, r)
return
}
}
switch s.action(r) {
case "attach":
s.Association[spec.TagID][*spec.ObjectID] = true
s.ok(w)
case "detach":
delete(s.Association[spec.TagID], *spec.ObjectID)
s.ok(w)
case "list-attached-tags":
var ids []string
for id, objs := range s.Association {
if objs[*spec.ObjectID] {
ids = append(ids, id)
}
}
s.ok(w, ids)
case "list-attached-objects":
var ids []internal.AssociatedObject
for id := range s.Association[spec.TagID] {
ids = append(ids, id)
}
s.ok(w, ids)
}
}

View File

@@ -4,7 +4,6 @@ go_library(
name = "go_default_library",
srcs = [
"categories.go",
"rest_client.go",
"tag_association.go",
"tags.go",
],
@@ -12,8 +11,9 @@ go_library(
importpath = "github.com/vmware/govmomi/vapi/tags",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/vmware/govmomi/vim25/soap:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25/types:go_default_library",
"//vendor/github.com/vmware/govmomi/vapi/internal:go_default_library",
"//vendor/github.com/vmware/govmomi/vapi/rest:go_default_library",
"//vendor/github.com/vmware/govmomi/vim25/mo:go_default_library",
],
)

View File

@@ -1,226 +1,162 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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.
*/
package tags
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/vmware/govmomi/vapi/internal"
)
const (
CategoryURL = "/com/vmware/cis/tagging/category"
ErrAlreadyExists = "already_exists"
)
type CategoryCreateSpec struct {
CreateSpec CategoryCreate `json:"create_spec"`
}
type CategoryUpdateSpec struct {
UpdateSpec CategoryUpdate `json:"update_spec,omitempty"`
}
type CategoryCreate struct {
AssociableTypes []string `json:"associable_types"`
Cardinality string `json:"cardinality"`
Description string `json:"description"`
Name string `json:"name"`
}
type CategoryUpdate struct {
AssociableTypes []string `json:"associable_types,omitempty"`
Cardinality string `json:"cardinality,omitempty"`
Description string `json:"description,omitempty"`
Name string `json:"name,omitempty"`
}
// Category provides methods to create, read, update, delete, and enumerate categories.
type Category struct {
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
Cardinality string `json:"cardinality"`
AssociableTypes []string `json:"associable_types"`
UsedBy []string `json:"used_by"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Cardinality string `json:"cardinality,omitempty"`
AssociableTypes []string `json:"associable_types,omitempty"`
UsedBy []string `json:"used_by,omitempty"`
}
type CategoryInfo struct {
Name string
CategoryID string
}
func (c *RestClient) CreateCategoryIfNotExist(ctx context.Context, name string, description string, categoryType string, multiValue bool) (*string, error) {
categories, err := c.GetCategoriesByName(ctx, name)
if err != nil {
return nil, err
}
if categories == nil {
var multiValueStr string
if multiValue {
multiValueStr = "MULTIPLE"
} else {
multiValueStr = "SINGLE"
func (c *Category) hasType(kind string) bool {
for _, k := range c.AssociableTypes {
if kind == k {
return true
}
categoryCreate := CategoryCreate{[]string{categoryType}, multiValueStr, description, name}
spec := CategoryCreateSpec{categoryCreate}
id, err := c.CreateCategory(ctx, &spec)
}
return false
}
// Patch merges Category changes from the given src.
// AssociableTypes can only be appended to and cannot shrink.
func (c *Category) Patch(src *Category) {
if src.Name != "" {
c.Name = src.Name
}
if src.Description != "" {
c.Description = src.Description
}
if src.Cardinality != "" {
c.Cardinality = src.Cardinality
}
// Note that in order to append to AssociableTypes any existing types must be included in their original order.
for _, kind := range src.AssociableTypes {
if !c.hasType(kind) {
c.AssociableTypes = append(c.AssociableTypes, kind)
}
}
}
// CreateCategory creates a new category and returns the category ID.
func (c *Manager) CreateCategory(ctx context.Context, category *Category) (string, error) {
// create avoids the annoyance of CreateTag requiring field keys to be included in the request,
// even though the field value can be empty.
type create struct {
Name string `json:"name"`
Description string `json:"description"`
Cardinality string `json:"cardinality"`
AssociableTypes []string `json:"associable_types"`
}
spec := struct {
Category create `json:"create_spec"`
}{
Category: create{
Name: category.Name,
Description: category.Description,
Cardinality: category.Cardinality,
AssociableTypes: category.AssociableTypes,
},
}
if spec.Category.AssociableTypes == nil {
// otherwise create fails with invalid_argument
spec.Category.AssociableTypes = []string{}
}
url := internal.URL(c, internal.CategoryPath)
var res string
return res, c.Do(ctx, url.Request(http.MethodPost, spec), &res)
}
// UpdateCategory can update one or more of the AssociableTypes, Cardinality, Description and Name fields.
func (c *Manager) UpdateCategory(ctx context.Context, category *Category) error {
spec := struct {
Category Category `json:"update_spec"`
}{
Category: Category{
AssociableTypes: category.AssociableTypes,
Cardinality: category.Cardinality,
Description: category.Description,
Name: category.Name,
},
}
url := internal.URL(c, internal.CategoryPath).WithID(category.ID)
return c.Do(ctx, url.Request(http.MethodPatch, spec), nil)
}
// DeleteCategory deletes an existing category.
func (c *Manager) DeleteCategory(ctx context.Context, category *Category) error {
url := internal.URL(c, internal.CategoryPath).WithID(category.ID)
return c.Do(ctx, url.Request(http.MethodDelete), nil)
}
// GetCategory fetches the category information for the given identifier.
// The id parameter can be a Category ID or Category Name.
func (c *Manager) GetCategory(ctx context.Context, id string) (*Category, error) {
if isName(id) {
cat, err := c.GetCategories(ctx)
if err != nil {
// in case there are two docker daemon try to create inventory category, query the category once again
if strings.Contains(err.Error(), "ErrAlreadyExists") {
if categories, err = c.GetCategoriesByName(ctx, name); err != nil {
return nil, fmt.Errorf("failed to get inventory category for %s", err)
}
} else {
return nil, fmt.Errorf("failed to create inventory category for %s", err)
return nil, err
}
for i := range cat {
if cat[i].Name == id {
return &cat[i], nil
}
} else {
return id, nil
}
}
if categories != nil {
return &categories[0].ID, nil
}
// should not happen
return nil, fmt.Errorf("failed to create inventory for it's existed, but could not query back. Please check system")
url := internal.URL(c, internal.CategoryPath).WithID(id)
var res Category
return &res, c.Do(ctx, url.Request(http.MethodGet), &res)
}
func (c *RestClient) CreateCategory(ctx context.Context, spec *CategoryCreateSpec) (*string, error) {
stream, _, status, err := c.call(ctx, http.MethodPost, CategoryURL, spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("create category failed with status code: %d, error message: %s", status, err)
}
type RespValue struct {
Value string
}
var pID RespValue
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return &(pID.Value), nil
// ListCategories returns all category IDs in the system.
func (c *Manager) ListCategories(ctx context.Context) ([]string, error) {
url := internal.URL(c, internal.CategoryPath)
var res []string
return res, c.Do(ctx, url.Request(http.MethodGet), &res)
}
func (c *RestClient) GetCategory(ctx context.Context, id string) (*Category, error) {
stream, _, status, err := c.call(ctx, http.MethodGet, fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("get category failed with status code: %d, error message: %s", status, err)
}
type RespValue struct {
Value Category
}
var pCategory RespValue
if err := json.NewDecoder(stream).Decode(&pCategory); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return &(pCategory.Value), nil
}
func (c *RestClient) UpdateCategory(ctx context.Context, id string, spec *CategoryUpdateSpec) error {
_, _, status, err := c.call(ctx, http.MethodPatch, fmt.Sprintf("%s/id:%s", CategoryURL, id), spec, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("update category failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) DeleteCategory(ctx context.Context, id string) error {
_, _, status, err := c.call(ctx, http.MethodDelete, fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("delete category failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) ListCategories(ctx context.Context) ([]string, error) {
stream, _, status, err := c.call(ctx, http.MethodGet, CategoryURL, nil, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("get categories failed with status code: %d, error message: %s", status, err)
}
type Categories struct {
Value []string
}
var pCategories Categories
if err := json.NewDecoder(stream).Decode(&pCategories); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return pCategories.Value, nil
}
func (c *RestClient) ListCategoriesByName(ctx context.Context) ([]CategoryInfo, error) {
categoryIds, err := c.ListCategories(ctx)
// GetCategories fetches an array of category information in the system.
func (c *Manager) GetCategories(ctx context.Context) ([]Category, error) {
ids, err := c.ListCategories(ctx)
if err != nil {
return nil, fmt.Errorf("get category failed for: %s", err)
}
var categoryInfoSlice []CategoryInfo
for _, cID := range categoryIds {
category, err := c.GetCategory(ctx, cID)
if err != nil {
return nil, fmt.Errorf("get category %s failed for %s", cID, err)
}
categoryCreate := &CategoryInfo{Name: category.Name, CategoryID: category.ID}
categoryInfoSlice = append(categoryInfoSlice, *categoryCreate)
}
return categoryInfoSlice, nil
}
func (c *RestClient) GetCategoriesByName(ctx context.Context, name string) ([]Category, error) {
categoryIds, err := c.ListCategories(ctx)
if err != nil {
return nil, fmt.Errorf("get category failed for: %s", err)
return nil, fmt.Errorf("list categories: %s", err)
}
var categories []Category
for _, cID := range categoryIds {
category, err := c.GetCategory(ctx, cID)
for _, id := range ids {
category, err := c.GetCategory(ctx, id)
if err != nil {
return nil, fmt.Errorf("get category %s failed for %s", cID, err)
}
if category.Name == name {
categories = append(categories, *category)
return nil, fmt.Errorf("get category %s: %s", id, err)
}
categories = append(categories, *category)
}
return categories, nil
}

View File

@@ -1,272 +0,0 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
package tags
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"github.com/vmware/govmomi/vim25/soap"
)
const (
RestPrefix = "/rest"
loginURL = "/com/vmware/cis/session"
sessionIDCookieName = "vmware-api-session-id"
)
type RestClient struct {
mu sync.Mutex
host string
scheme string
endpoint *url.URL
user *url.Userinfo
HTTP *http.Client
cookies []*http.Cookie
}
func NewClient(u *url.URL, insecure bool, thumbprint string) *RestClient {
endpoint := &url.URL{}
*endpoint = *u
endpoint.Path = RestPrefix
// Ignore "#" anchor
endpoint.Fragment = ""
sc := soap.NewClient(endpoint, insecure)
if thumbprint != "" {
sc.SetThumbprint(endpoint.Host, thumbprint)
}
user := endpoint.User
endpoint.User = nil
return &RestClient{
endpoint: endpoint,
user: user,
host: endpoint.Host,
scheme: endpoint.Scheme,
HTTP: &sc.Client,
}
}
// NewClientWithSessionID creates a new REST client with a supplied session ID
// to re-connect to existing sessions.
//
// Note that the session is not checked for validity - to check for a valid
// session after creating the client, use the Valid method. If the session is
// no longer valid and the session needs to be re-saved, Login should be called
// again before calling SessionID to extract the new session ID. Clients
// created with this function function work in the exact same way as clients
// created with NewClient, including supporting re-login on invalid sessions on
// all SDK calls.
func NewClientWithSessionID(u *url.URL, insecure bool, thumbprint string, sessionID string) *RestClient {
c := NewClient(u, insecure, thumbprint)
c.SetSessionID(sessionID)
return c
}
func (c *RestClient) encodeData(data interface{}) (*bytes.Buffer, error) {
params := bytes.NewBuffer(nil)
if data != nil {
if err := json.NewEncoder(params).Encode(data); err != nil {
return nil, err
}
}
return params, nil
}
func (c *RestClient) call(ctx context.Context, method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
// Logger.Debugf("%s: %s, headers: %+v", method, path, headers)
params, err := c.encodeData(data)
if err != nil {
return nil, nil, -1, err
}
if data != nil {
if headers == nil {
headers = make(map[string][]string)
}
headers["Content-Type"] = []string{"application/json"}
}
body, hdr, statusCode, err := c.clientRequest(ctx, method, path, params, headers)
if statusCode == http.StatusUnauthorized && strings.Contains(err.Error(), "This method requires authentication") {
c.Login(ctx)
return c.clientRequest(ctx, method, path, params, headers)
}
return body, hdr, statusCode, err
}
func (c *RestClient) clientRequest(ctx context.Context, method, path string, in io.Reader, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
expectedPayload := (method == http.MethodPost || method == http.MethodPut)
if expectedPayload && in == nil {
in = bytes.NewReader([]byte{})
}
req, err := c.newRequest(method, path, in)
if err != nil {
return nil, nil, -1, err
}
req = req.WithContext(ctx)
c.mu.Lock()
if c.cookies != nil {
req.AddCookie(c.cookies[0])
}
c.mu.Unlock()
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
if expectedPayload && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
resp, err := c.HTTP.Do(req)
return c.handleResponse(resp, err)
}
func (c *RestClient) handleResponse(resp *http.Response, err error) (io.ReadCloser, http.Header, int, error) {
statusCode := -1
if resp != nil {
statusCode = resp.StatusCode
}
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, nil, statusCode, err
}
return nil, nil, statusCode, err
}
if statusCode < http.StatusOK || statusCode >= http.StatusBadRequest {
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, nil, statusCode, err
}
if len(body) == 0 {
return nil, nil, statusCode, err
}
return nil, nil, statusCode, fmt.Errorf("error response: %s", bytes.TrimSpace(body))
}
return resp.Body, resp.Header, statusCode, nil
}
func (c *RestClient) Login(ctx context.Context) error {
request, err := c.newRequest(http.MethodPost, loginURL, nil)
if err != nil {
return err
}
if c.user != nil {
password, _ := c.user.Password()
request.SetBasicAuth(c.user.Username(), password)
}
resp, err := c.HTTP.Do(request)
if err != nil {
return err
}
if resp == nil {
return err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return err
}
c.cookies = resp.Cookies()
return nil
}
func (c *RestClient) Logout(ctx context.Context) error {
_, _, status, err := c.call(ctx, http.MethodDelete, loginURL, nil, nil)
if status != http.StatusOK || err != nil {
return err
}
c.SetSessionID("")
return nil
}
func (c *RestClient) newRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
return http.NewRequest(method, c.endpoint.String()+urlStr, body)
}
// SessionID returns the current session ID of the REST client. An empty string
// means there was no session cookie currently loaded.
func (c *RestClient) SessionID() string {
for _, cookie := range c.cookies {
if cookie.Name == sessionIDCookieName {
return cookie.Value
}
}
return ""
}
// SetSessionID sets the session cookie with the supplied session ID.
//
// This does not necessarily mean the session is valid. The session should be
// checked with Valid before proceeding, and logged back in if it has expired.
//
// This function will overwrite any existing session.
func (c *RestClient) SetSessionID(sessionID string) {
idx := -1
for i, cookie := range c.cookies {
if cookie.Name == sessionIDCookieName {
idx = i
}
}
sessionCookie := &http.Cookie{
Name: sessionIDCookieName,
Value: sessionID,
Path: RestPrefix,
}
if idx > -1 {
c.cookies[idx] = sessionCookie
} else {
c.cookies = append(c.cookies, sessionCookie)
}
}
// Valid checks to see if the session cookies in a REST client are still valid.
// This should be used when restoring a session to determine if a new login is
// necessary.
func (c *RestClient) Valid(ctx context.Context) bool {
_, _, statusCode, err := c.clientRequest(ctx, http.MethodPost, loginURL+"?~action=get", nil, nil)
if err != nil {
return false
}
if statusCode == http.StatusOK {
return true
}
return false
}

View File

@@ -1,139 +1,108 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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
vUnless 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 tags
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/govmomi/vapi/internal"
"github.com/vmware/govmomi/vim25/mo"
)
const (
TagAssociationURL = "/com/vmware/cis/tagging/tag-association"
)
type AssociatedObject struct {
ID string `json:"id"`
Type string `json:"type"`
}
type TagAssociationSpec struct {
ObjectID *AssociatedObject `json:"object_id,omitempty"`
TagID *string `json:"tag_id,omitempty"`
}
type AttachedTagsInfo struct {
Name string
TagID string
}
func (c *RestClient) getAssociatedObject(ref *types.ManagedObjectReference) *AssociatedObject {
if ref == nil {
return nil
}
object := AssociatedObject{
ID: ref.Value,
Type: ref.Type,
}
return &object
}
func (c *RestClient) getAssociationSpec(tagID *string, ref *types.ManagedObjectReference) *TagAssociationSpec {
object := c.getAssociatedObject(ref)
spec := TagAssociationSpec{
TagID: tagID,
ObjectID: object,
}
return &spec
}
func (c *RestClient) AttachTagToObject(ctx context.Context, tagID string, ref *types.ManagedObjectReference) error {
spec := c.getAssociationSpec(&tagID, ref)
_, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s?~action=attach", TagAssociationURL), *spec, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("attach tag failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) DetachTagFromObject(ctx context.Context, tagID string, ref *types.ManagedObjectReference) error {
spec := c.getAssociationSpec(&tagID, ref)
_, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s?~action=detach", TagAssociationURL), *spec, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("detach tag failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) ListAttachedTags(ctx context.Context, ref *types.ManagedObjectReference) ([]string, error) {
spec := c.getAssociationSpec(nil, ref)
stream, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s?~action=list-attached-tags", TagAssociationURL), *spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("detach tag failed with status code: %d, error message: %s", status, err)
}
type RespValue struct {
Value []string
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return pTag.Value, nil
}
func (c *RestClient) ListAttachedTagsByName(ctx context.Context, ref *types.ManagedObjectReference) ([]AttachedTagsInfo, error) {
tagIds, err := c.ListAttachedTags(ctx, ref)
if err != nil {
return nil, fmt.Errorf("get attached tag failed for: %s", err)
}
var attachedTagsInfoSlice []AttachedTagsInfo
for _, cID := range tagIds {
tag, err := c.GetTag(ctx, cID)
func (c *Manager) tagID(ctx context.Context, id string) (string, error) {
if isName(id) {
tag, err := c.GetTag(ctx, id)
if err != nil {
return nil, fmt.Errorf("get tag %s failed for %s", cID, err)
return "", err
}
attachedTagsCreate := &AttachedTagsInfo{Name: tag.Name, TagID: tag.ID}
attachedTagsInfoSlice = append(attachedTagsInfoSlice, *attachedTagsCreate)
return tag.ID, nil
}
return attachedTagsInfoSlice, nil
return id, nil
}
func (c *RestClient) ListAttachedObjects(ctx context.Context, tagID string) ([]AssociatedObject, error) {
spec := c.getAssociationSpec(&tagID, nil)
stream, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s?~action=list-attached-objects", TagAssociationURL), *spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("list object failed with status code: %d, error message: %s", status, err)
// AttachTag attaches a tag ID to a managed object.
func (c *Manager) AttachTag(ctx context.Context, tagID string, ref mo.Reference) error {
id, err := c.tagID(ctx, tagID)
if err != nil {
return err
}
type RespValue struct {
Value []AssociatedObject
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return pTag.Value, nil
spec := internal.NewAssociation(id, ref)
url := internal.URL(c, internal.AssociationPath).WithAction("attach")
return c.Do(ctx, url.Request(http.MethodPost, spec), nil)
}
// DetachTag detaches a tag ID from a managed object.
// If the tag is already removed from the object, then this operation is a no-op and an error will not be thrown.
func (c *Manager) DetachTag(ctx context.Context, tagID string, ref mo.Reference) error {
id, err := c.tagID(ctx, tagID)
if err != nil {
return err
}
spec := internal.NewAssociation(id, ref)
url := internal.URL(c, internal.AssociationPath).WithAction("detach")
return c.Do(ctx, url.Request(http.MethodPost, spec), nil)
}
// ListAttachedTags fetches the array of tag IDs attached to the given object.
func (c *Manager) ListAttachedTags(ctx context.Context, ref mo.Reference) ([]string, error) {
spec := internal.NewAssociation("", ref)
url := internal.URL(c, internal.AssociationPath).WithAction("list-attached-tags")
var res []string
return res, c.Do(ctx, url.Request(http.MethodPost, spec), &res)
}
// GetAttachedTags fetches the array of tags attached to the given object.
func (c *Manager) GetAttachedTags(ctx context.Context, ref mo.Reference) ([]Tag, error) {
ids, err := c.ListAttachedTags(ctx, ref)
if err != nil {
return nil, fmt.Errorf("get attached tags %s: %s", ref, err)
}
var info []Tag
for _, id := range ids {
tag, err := c.GetTag(ctx, id)
if err != nil {
return nil, fmt.Errorf("get tag %s: %s", id, err)
}
info = append(info, *tag)
}
return info, nil
}
// ListAttachedObjects fetches the array of attached objects for the given tag ID.
func (c *Manager) ListAttachedObjects(ctx context.Context, tagID string) ([]mo.Reference, error) {
id, err := c.tagID(ctx, tagID)
if err != nil {
return nil, err
}
spec := internal.Association{
TagID: id,
}
url := internal.URL(c, internal.AssociationPath).WithAction("list-attached-objects")
var res []internal.AssociatedObject
if err := c.Do(ctx, url.Request(http.MethodPost, spec), &res); err != nil {
return nil, err
}
refs := make([]mo.Reference, len(res))
for i := range res {
refs[i] = res[i]
}
return refs, nil
}

View File

@@ -1,252 +1,202 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// 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.
/*
Copyright (c) 2018 VMware, Inc. All Rights Reserved.
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.
*/
package tags
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/vmware/govmomi/vapi/internal"
"github.com/vmware/govmomi/vapi/rest"
)
const (
TagURL = "/com/vmware/cis/tagging/tag"
)
type TagCreateSpec struct {
CreateSpec TagCreate `json:"create_spec"`
// Manager extends rest.Client, adding tag related methods.
type Manager struct {
*rest.Client
}
type TagCreate struct {
CategoryID string `json:"category_id"`
Description string `json:"description"`
Name string `json:"name"`
}
type TagUpdateSpec struct {
UpdateSpec TagUpdate `json:"update_spec,omitempty"`
}
type TagUpdate struct {
Description string `json:"description,omitempty"`
Name string `json:"name,omitempty"`
}
type Tag struct {
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
CategoryID string `json:"category_id"`
UsedBy []string `json:"used_by"`
}
func (c *RestClient) CreateTagIfNotExist(ctx context.Context, name string, description string, categoryID string) (*string, error) {
tagCreate := TagCreate{categoryID, description, name}
spec := TagCreateSpec{tagCreate}
id, err := c.CreateTag(ctx, &spec)
if err == nil {
return id, nil
// NewManager creates a new Manager instance with the given client.
func NewManager(client *rest.Client) *Manager {
return &Manager{
Client: client,
}
// if already exists, query back
if strings.Contains(err.Error(), ErrAlreadyExists) {
tagObjs, err := c.GetTagByNameForCategory(ctx, name, categoryID)
}
// isName returns true if the id is not a urn.
func isName(id string) bool {
return !strings.HasPrefix(id, "urn:")
}
// Tag provides methods to create, read, update, delete, and enumerate tags.
type Tag struct {
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
Name string `json:"name,omitempty"`
CategoryID string `json:"category_id,omitempty"`
UsedBy []string `json:"used_by,omitempty"`
}
// Patch merges updates from the given src.
func (t *Tag) Patch(src *Tag) {
if src.Name != "" {
t.Name = src.Name
}
if src.Description != "" {
t.Description = src.Description
}
if src.CategoryID != "" {
t.CategoryID = src.CategoryID
}
}
// CreateTag creates a new tag with the given Name, Description and CategoryID.
func (c *Manager) CreateTag(ctx context.Context, tag *Tag) (string, error) {
// create avoids the annoyance of CreateTag requiring a "description" key to be included in the request,
// even though the field value can be empty.
type create struct {
Name string `json:"name"`
Description string `json:"description"`
CategoryID string `json:"category_id"`
}
spec := struct {
Tag create `json:"create_spec"`
}{
Tag: create{
Name: tag.Name,
Description: tag.Description,
CategoryID: tag.CategoryID,
},
}
if isName(tag.CategoryID) {
cat, err := c.GetCategory(ctx, tag.CategoryID)
if err != nil {
return "", err
}
spec.Tag.CategoryID = cat.ID
}
url := internal.URL(c, internal.TagPath)
var res string
return res, c.Do(ctx, url.Request(http.MethodPost, spec), &res)
}
// UpdateTag can update one or both of the tag Description and Name fields.
func (c *Manager) UpdateTag(ctx context.Context, tag *Tag) error {
spec := struct {
Tag Tag `json:"update_spec"`
}{
Tag: Tag{
Name: tag.Name,
Description: tag.Description,
},
}
url := internal.URL(c, internal.TagPath).WithID(tag.ID)
return c.Do(ctx, url.Request(http.MethodPatch, spec), nil)
}
// DeleteTag deletes an existing tag.
func (c *Manager) DeleteTag(ctx context.Context, tag *Tag) error {
url := internal.URL(c, internal.TagPath).WithID(tag.ID)
return c.Do(ctx, url.Request(http.MethodDelete), nil)
}
// GetTag fetches the tag information for the given identifier.
// The id parameter can be a Tag ID or Tag Name.
func (c *Manager) GetTag(ctx context.Context, id string) (*Tag, error) {
if isName(id) {
tags, err := c.GetTags(ctx)
if err != nil {
return nil, err
}
if tagObjs != nil {
return &tagObjs[0].ID, nil
for i := range tags {
if tags[i].Name == id {
return &tags[i], nil
}
}
// should not happen
return nil, fmt.Errorf("failed to create tag for it's existed, but could not query back. Please check system")
}
return nil, fmt.Errorf("created tag failed for %s", err)
url := internal.URL(c, internal.TagPath).WithID(id)
var res Tag
return &res, c.Do(ctx, url.Request(http.MethodGet), &res)
}
func (c *RestClient) DeleteTagIfNoObjectAttached(ctx context.Context, id string) error {
objs, err := c.ListAttachedObjects(ctx, id)
if err != nil {
return err
}
if len(objs) > 0 {
return fmt.Errorf("tag %s related objects is not empty, do not delete it", id)
}
return c.DeleteTag(ctx, id)
// ListTags returns all tag IDs in the system.
func (c *Manager) ListTags(ctx context.Context) ([]string, error) {
url := internal.URL(c, internal.TagPath)
var res []string
return res, c.Do(ctx, url.Request(http.MethodGet), &res)
}
func (c *RestClient) CreateTag(ctx context.Context, spec *TagCreateSpec) (*string, error) {
stream, _, status, err := c.call(ctx, http.MethodPost, TagURL, spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("create tag failed with status code: %d, error message: %s", status, err)
}
type RespValue struct {
Value string
}
var pID RespValue
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return &pID.Value, nil
}
func (c *RestClient) GetTag(ctx context.Context, id string) (*Tag, error) {
stream, _, status, err := c.call(ctx, http.MethodGet, fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("get tag failed with status code: %d, error message: %s", status, err)
}
type RespValue struct {
Value Tag
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return &(pTag.Value), nil
}
func (c *RestClient) UpdateTag(ctx context.Context, id string, spec *TagUpdateSpec) error {
_, _, status, err := c.call(ctx, http.MethodPatch, fmt.Sprintf("%s/id:%s", TagURL, id), spec, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("update tag failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) DeleteTag(ctx context.Context, id string) error {
_, _, status, err := c.call(ctx, http.MethodDelete, fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
if status != http.StatusOK || err != nil {
return fmt.Errorf("delete tag failed with status code: %d, error message: %s", status, err)
}
return nil
}
func (c *RestClient) ListTags(ctx context.Context) ([]string, error) {
stream, _, status, err := c.call(ctx, http.MethodGet, TagURL, nil, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("get tags failed with status code: %d, error message: %s", status, err)
}
return c.handleTagIDList(stream)
}
type TagsInfo struct {
Name string
TagID string
}
func (c *RestClient) ListTagsByName(ctx context.Context) ([]TagsInfo, error) {
tagIds, err := c.ListTags(ctx)
// GetTags fetches an array of tag information in the system.
func (c *Manager) GetTags(ctx context.Context) ([]Tag, error) {
ids, err := c.ListTags(ctx)
if err != nil {
return nil, fmt.Errorf("get tags failed for: %s", err)
}
var tagsInfoSlice []TagsInfo
for _, cID := range tagIds {
tag, err := c.GetTag(ctx, cID)
if err != nil {
return nil, fmt.Errorf("get category %s failed for %s", cID, err)
}
tagsCreate := &TagsInfo{Name: tag.Name, TagID: tag.ID}
tagsInfoSlice = append(tagsInfoSlice, *tagsCreate)
}
return tagsInfoSlice, nil
}
func (c *RestClient) ListTagsForCategory(ctx context.Context, id string) ([]string, error) {
type PostCategory struct {
ID string `json:"category_id"`
}
spec := PostCategory{id}
stream, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s/id:%s?~action=list-tags-for-category", TagURL, id), spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("list tags for category failed with status code: %d, error message: %s", status, err)
}
return c.handleTagIDList(stream)
}
func (c *RestClient) ListTagsInfoForCategory(ctx context.Context, id string) ([]TagsInfo, error) {
type PostCategory struct {
ID string `json:"category_id"`
}
spec := PostCategory{id}
stream, _, status, err := c.call(ctx, http.MethodPost, fmt.Sprintf("%s/id:%s?~action=list-tags-for-category", TagURL, id), spec, nil)
if status != http.StatusOK || err != nil {
return nil, fmt.Errorf("list tags for category failed with status code: %d, error message: %s", status, err)
}
var tagsInfoSlice []TagsInfo
tmp, err := c.handleTagIDList(stream)
for _, item := range tmp {
tag, err := c.GetTag(ctx, item)
if err != nil {
return nil, fmt.Errorf("get category %s failed for %s", item, err)
}
tagsCreate := &TagsInfo{Name: tag.Name, TagID: tag.ID}
tagsInfoSlice = append(tagsInfoSlice, *tagsCreate)
}
return tagsInfoSlice, nil
}
func (c *RestClient) handleTagIDList(stream io.ReadCloser) ([]string, error) {
type Tags struct {
Value []string
}
var pTags Tags
if err := json.NewDecoder(stream).Decode(&pTags); err != nil {
return nil, fmt.Errorf("decode response body failed for: %s", err)
}
return pTags.Value, nil
}
// Get tag through tag name and category id
func (c *RestClient) GetTagByNameForCategory(ctx context.Context, name string, id string) ([]Tag, error) {
tagIds, err := c.ListTagsForCategory(ctx, id)
if err != nil {
return nil, fmt.Errorf("get tag failed for %s", err)
}
var tags []Tag
for _, tID := range tagIds {
tag, err := c.GetTag(ctx, tID)
for _, id := range ids {
tag, err := c.GetTag(ctx, id)
if err != nil {
return nil, fmt.Errorf("get tag %s failed for %s", tID, err)
}
if tag.Name == name {
tags = append(tags, *tag)
return nil, fmt.Errorf("get category %s failed for %s", id, err)
}
tags = append(tags, *tag)
}
return tags, nil
}
// The id parameter can be a Category ID or Category Name.
func (c *Manager) ListTagsForCategory(ctx context.Context, id string) ([]string, error) {
if isName(id) {
cat, err := c.GetCategory(ctx, id)
if err != nil {
return nil, err
}
id = cat.ID
}
body := struct {
ID string `json:"category_id"`
}{id}
url := internal.URL(c, internal.TagPath).WithID(id).WithAction("list-tags-for-category")
var res []string
return res, c.Do(ctx, url.Request(http.MethodPost, body), &res)
}
// The id parameter can be a Category ID or Category Name.
func (c *Manager) GetTagsForCategory(ctx context.Context, id string) ([]Tag, error) {
ids, err := c.ListTagsForCategory(ctx, id)
if err != nil {
return nil, err
}
var tags []Tag
for _, id := range ids {
tag, err := c.GetTag(ctx, id)
if err != nil {
return nil, fmt.Errorf("get tag %s: %s", id, err)
}
tags = append(tags, *tag)
}
return tags, nil
}

View File

@@ -449,12 +449,51 @@ func (c *Client) UnmarshalJSON(b []byte) error {
return nil
}
func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) {
if nil == ctx || nil == ctx.Done() { // ctx.Done() is for ctx
return c.Client.Do(req)
type kindContext struct{}
func (c *Client) Do(ctx context.Context, req *http.Request, f func(*http.Response) error) error {
if ctx == nil {
ctx = context.Background()
}
// Create debugging context for this round trip
d := c.d.newRoundTrip()
if d.enabled() {
defer d.done()
}
return c.Client.Do(req.WithContext(ctx))
if c.UserAgent != "" {
req.Header.Set(`User-Agent`, c.UserAgent)
}
if d.enabled() {
d.debugRequest(req)
}
tstart := time.Now()
res, err := c.Client.Do(req.WithContext(ctx))
tstop := time.Now()
if d.enabled() {
var name string
if kind, ok := ctx.Value(kindContext{}).(HasFault); ok {
name = fmt.Sprintf("%T", kind)
} else {
name = fmt.Sprintf("%s %s", req.Method, req.URL)
}
d.logf("%6dms (%s)", tstop.Sub(tstart)/time.Millisecond, name)
}
if err != nil {
return err
}
defer res.Body.Close()
if d.enabled() {
d.debugResponse(res)
}
return f(res)
}
// Signer can be implemented by soap.Header.Security to sign requests.
@@ -493,12 +532,6 @@ func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error
reqEnv.Header = &h // XML marshal header only if a field is set
}
// Create debugging context for this round trip
d := c.d.newRoundTrip()
if d.enabled() {
defer d.done()
}
if signer, ok := h.Security.(Signer); ok {
b, err = signer.Sign(reqEnv)
if err != nil {
@@ -517,8 +550,6 @@ func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error
panic(err)
}
req = req.WithContext(ctx)
req.Header.Set(`Content-Type`, `text/xml; charset="utf-8"`)
action := h.Action
@@ -527,54 +558,29 @@ func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error
}
req.Header.Set(`SOAPAction`, action)
if c.UserAgent != "" {
req.Header.Set(`User-Agent`, c.UserAgent)
}
return c.Do(context.WithValue(ctx, kindContext{}, resBody), req, func(res *http.Response) error {
switch res.StatusCode {
case http.StatusOK:
// OK
case http.StatusInternalServerError:
// Error, but typically includes a body explaining the error
default:
return errors.New(res.Status)
}
if d.enabled() {
d.debugRequest(req)
}
dec := xml.NewDecoder(res.Body)
dec.TypeFunc = types.TypeFunc()
err = dec.Decode(&resEnv)
if err != nil {
return err
}
tstart := time.Now()
res, err := c.do(ctx, req)
tstop := time.Now()
if f := resBody.Fault(); f != nil {
return WrapSoapFault(f)
}
if d.enabled() {
d.logf("%6dms (%T)", tstop.Sub(tstart)/time.Millisecond, resBody)
}
if err != nil {
return err
}
if d.enabled() {
d.debugResponse(res)
}
// Close response regardless of what happens next
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
// OK
case http.StatusInternalServerError:
// Error, but typically includes a body explaining the error
default:
return errors.New(res.Status)
}
dec := xml.NewDecoder(res.Body)
dec.TypeFunc = types.TypeFunc()
err = dec.Decode(&resEnv)
if err != nil {
return err
}
if f := resBody.Fault(); f != nil {
return WrapSoapFault(f)
}
return err
})
}
func (c *Client) CloseIdleConnections() {

View File

@@ -21,6 +21,7 @@ import (
"io"
"net/http"
"net/http/httputil"
"strings"
"sync/atomic"
"time"
@@ -69,6 +70,14 @@ func (d *debugRoundTrip) newFile(suffix string) io.WriteCloser {
return debug.NewFile(fmt.Sprintf("%d-%04d.%s", d.cn, d.rn, suffix))
}
func (d *debugRoundTrip) ext(h http.Header) string {
ext := "xml"
if strings.Contains(h.Get("Content-Type"), "/json") {
ext = "json"
}
return ext
}
func (d *debugRoundTrip) debugRequest(req *http.Request) {
if d == nil {
return
@@ -83,7 +92,7 @@ func (d *debugRoundTrip) debugRequest(req *http.Request) {
wc.Close()
// Capture body
wc = d.newFile("req.xml")
wc = d.newFile("req." + d.ext(req.Header))
req.Body = newTeeReader(req.Body, wc)
// Delay closing until marked done
@@ -104,7 +113,7 @@ func (d *debugRoundTrip) debugResponse(res *http.Response) {
wc.Close()
// Capture body
wc = d.newFile("res.xml")
wc = d.newFile("res." + d.ext(res.Header))
res.Body = newTeeReader(res.Body, wc)
// Delay closing until marked done