1// Copyright 2017 VMware, Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package tags
16
17import (
18	"context"
19	"encoding/json"
20	"fmt"
21	"io"
22	"net/http"
23	"regexp"
24	"strings"
25
26	"github.com/sirupsen/logrus"
27	"github.com/pkg/errors"
28)
29
30const (
31	TagURL = "/com/vmware/cis/tagging/tag"
32)
33
34type TagCreateSpec struct {
35	CreateSpec TagCreate `json:"create_spec"`
36}
37
38type TagCreate struct {
39	CategoryID  string `json:"category_id"`
40	Description string `json:"description"`
41	Name        string `json:"name"`
42}
43
44type TagUpdateSpec struct {
45	UpdateSpec TagUpdate `json:"update_spec"`
46}
47
48type TagUpdate struct {
49	Description string `json:"description"`
50	Name        string `json:"name"`
51}
52
53type Tag struct {
54	ID          string   `json:"id"`
55	Description string   `json:"description"`
56	Name        string   `json:"name"`
57	CategoryID  string   `json:"category_id"`
58	UsedBy      []string `json:"used_by"`
59}
60
61var Logger = logrus.New()
62
63func (c *RestClient) CreateTagIfNotExist(ctx context.Context, name string, description string, categoryID string) (*string, error) {
64	tagCreate := TagCreate{categoryID, description, name}
65	spec := TagCreateSpec{tagCreate}
66	id, err := c.CreateTag(ctx, &spec)
67	if err == nil {
68		return id, nil
69	}
70	Logger.Debugf("Created tag %s failed for %s", errors.WithStack(err))
71	// if already exists, query back
72	if strings.Contains(err.Error(), ErrAlreadyExists) {
73		tagObjs, err := c.GetTagByNameForCategory(ctx, name, categoryID)
74		if err != nil {
75			return nil, errors.Wrapf(err, "failed to query tag %s for category %s", name, categoryID)
76		}
77		if tagObjs != nil {
78			return &tagObjs[0].ID, nil
79		}
80
81		// should not happen
82		return nil, errors.New("Failed to create tag for it's existed, but could not query back. Please check system")
83	}
84
85	return nil, errors.Wrap(err, "failed to create tag")
86}
87
88func (c *RestClient) DeleteTagIfNoObjectAttached(ctx context.Context, id string) error {
89	objs, err := c.ListAttachedObjects(ctx, id)
90	if err != nil {
91		return errors.Wrap(err, "failed to delete tag")
92	}
93	if objs != nil && len(objs) > 0 {
94		Logger.Debugf("tag %s related objects is not empty, do not delete it.", id)
95		return nil
96	}
97	return c.DeleteTag(ctx, id)
98}
99
100func (c *RestClient) CreateTag(ctx context.Context, spec *TagCreateSpec) (*string, error) {
101	Logger.Debugf("Create Tag %v", spec)
102	stream, _, status, err := c.call(ctx, "POST", TagURL, spec, nil)
103
104	Logger.Debugf("Get status code: %d", status)
105	if status != http.StatusOK || err != nil {
106		Logger.Debugf("Create tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
107		return nil, errors.Wrapf(err, "Status code: %d", status)
108	}
109
110	type RespValue struct {
111		Value string
112	}
113
114	var pID RespValue
115	if err := json.NewDecoder(stream).Decode(&pID); err != nil {
116		Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
117		return nil, errors.Wrap(err, "create tag failed")
118	}
119	return &pID.Value, nil
120}
121
122func (c *RestClient) GetTag(ctx context.Context, id string) (*Tag, error) {
123	Logger.Debugf("Get tag %s", id)
124
125	stream, _, status, err := c.call(ctx, "GET", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
126
127	if status != http.StatusOK || err != nil {
128		Logger.Debugf("Get tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
129		return nil, errors.Wrapf(err, "Status code: %d", status)
130	}
131
132	type RespValue struct {
133		Value Tag
134	}
135
136	var pTag RespValue
137	if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
138		Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
139		return nil, errors.Wrapf(err, "failed to get tag %s", id)
140	}
141	return &(pTag.Value), nil
142}
143
144func (c *RestClient) UpdateTag(ctx context.Context, id string, spec *TagUpdateSpec) error {
145	Logger.Debugf("Update tag %v", spec)
146	_, _, status, err := c.call(ctx, "PATCH", fmt.Sprintf("%s/id:%s", TagURL, id), spec, nil)
147
148	Logger.Debugf("Get status code: %d", status)
149	if status != http.StatusOK || err != nil {
150		Logger.Debugf("Update tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
151		return errors.Wrapf(err, "Status code: %d", status)
152	}
153
154	return nil
155}
156
157func (c *RestClient) DeleteTag(ctx context.Context, id string) error {
158	Logger.Debugf("Delete tag %s", id)
159
160	_, _, status, err := c.call(ctx, "DELETE", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
161
162	if status != http.StatusOK || err != nil {
163		Logger.Debugf("Delete tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
164		return errors.Wrapf(err, "Status code: %d", status)
165	}
166	return nil
167}
168
169func (c *RestClient) ListTags(ctx context.Context) ([]string, error) {
170	Logger.Debugf("List all tags")
171
172	stream, _, status, err := c.call(ctx, "GET", TagURL, nil, nil)
173
174	if status != http.StatusOK || err != nil {
175		Logger.Debugf("Get tags failed with status code: %s, error message: %s", status, errors.WithStack(err))
176		return nil, errors.Wrapf(err, "Status code: %d", status)
177	}
178
179	return c.handleTagIDList(stream)
180}
181
182func (c *RestClient) ListTagsForCategory(ctx context.Context, id string) ([]string, error) {
183	Logger.Debugf("List tags for category: %s", id)
184
185	type PostCategory struct {
186		CId string `json:"category_id"`
187	}
188	spec := PostCategory{id}
189	stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s/id:%s?~action=list-tags-for-category", TagURL, id), spec, nil)
190
191	if status != http.StatusOK || err != nil {
192		Logger.Debugf("List tags for category failed with status code: %s, error message: %s", status, errors.WithStack(err))
193		return nil, errors.Wrapf(err, "Status code: %d", status)
194	}
195
196	return c.handleTagIDList(stream)
197}
198
199func (c *RestClient) handleTagIDList(stream io.ReadCloser) ([]string, error) {
200	type Tags struct {
201		Value []string
202	}
203
204	var pTags Tags
205	if err := json.NewDecoder(stream).Decode(&pTags); err != nil {
206		Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
207		return nil, errors.Wrap(err, "failed to decode json")
208	}
209	return pTags.Value, nil
210}
211
212// Get tag through tag name and category id
213func (c *RestClient) GetTagByNameForCategory(ctx context.Context, name string, id string) ([]Tag, error) {
214	Logger.Debugf("Get tag %s for category %s", name, id)
215	tagIds, err := c.ListTagsForCategory(ctx, id)
216	if err != nil {
217		Logger.Debugf("Get tag failed for %s", errors.WithStack(err))
218		return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
219	}
220
221	var tags []Tag
222	for _, tID := range tagIds {
223		tag, err := c.GetTag(ctx, tID)
224		if err != nil {
225			Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
226			return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
227		}
228		if tag.Name == name {
229			tags = append(tags, *tag)
230		}
231	}
232	return tags, nil
233}
234
235// Get attached tags through tag name pattern
236func (c *RestClient) GetAttachedTagsByNamePattern(ctx context.Context, namePattern string, objID string, objType string) ([]Tag, error) {
237	tagIds, err := c.ListAttachedTags(ctx, objID, objType)
238	if err != nil {
239		Logger.Debugf("Get attached tags failed for %s", errors.WithStack(err))
240		return nil, errors.Wrap(err, "get attached tags failed")
241	}
242
243	var validName = regexp.MustCompile(namePattern)
244	var tags []Tag
245	for _, tID := range tagIds {
246		tag, err := c.GetTag(ctx, tID)
247		if err != nil {
248			Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
249		}
250		if validName.MatchString(tag.Name) {
251			tags = append(tags, *tag)
252		}
253	}
254	return tags, nil
255}
256