1package godo
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"net/http"
8	"path"
9)
10
11const (
12	// DefaultProject is the ID you should use if you are working with your
13	// default project.
14	DefaultProject = "default"
15
16	projectsBasePath = "/v2/projects"
17)
18
19// ProjectsService is an interface for creating and managing Projects with the DigitalOcean API.
20// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Projects
21type ProjectsService interface {
22	List(context.Context, *ListOptions) ([]Project, *Response, error)
23	GetDefault(context.Context) (*Project, *Response, error)
24	Get(context.Context, string) (*Project, *Response, error)
25	Create(context.Context, *CreateProjectRequest) (*Project, *Response, error)
26	Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error)
27	Delete(context.Context, string) (*Response, error)
28
29	ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error)
30	AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error)
31}
32
33// ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API.
34type ProjectsServiceOp struct {
35	client *Client
36}
37
38// Project represents a DigitalOcean Project configuration.
39type Project struct {
40	ID          string `json:"id"`
41	OwnerUUID   string `json:"owner_uuid"`
42	OwnerID     uint64 `json:"owner_id"`
43	Name        string `json:"name"`
44	Description string `json:"description"`
45	Purpose     string `json:"purpose"`
46	Environment string `json:"environment"`
47	IsDefault   bool   `json:"is_default"`
48	CreatedAt   string `json:"created_at"`
49	UpdatedAt   string `json:"updated_at"`
50}
51
52// String creates a human-readable description of a Project.
53func (p Project) String() string {
54	return Stringify(p)
55}
56
57// CreateProjectRequest represents the request to create a new project.
58type CreateProjectRequest struct {
59	Name        string `json:"name"`
60	Description string `json:"description"`
61	Purpose     string `json:"purpose"`
62	Environment string `json:"environment"`
63}
64
65// UpdateProjectRequest represents the request to update project information.
66// This type expects certain attribute types, but is built this way to allow
67// nil values as well. See `updateProjectRequest` for the "real" types.
68type UpdateProjectRequest struct {
69	Name        interface{}
70	Description interface{}
71	Purpose     interface{}
72	Environment interface{}
73	IsDefault   interface{}
74}
75
76type updateProjectRequest struct {
77	Name        *string `json:"name"`
78	Description *string `json:"description"`
79	Purpose     *string `json:"purpose"`
80	Environment *string `json:"environment"`
81	IsDefault   *bool   `json:"is_default"`
82}
83
84// MarshalJSON takes an UpdateRequest and converts it to the "typed" request
85// which is sent to the projects API. This is a PATCH request, which allows
86// partial attributes, so `null` values are OK.
87func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) {
88	d := &updateProjectRequest{}
89	if str, ok := upr.Name.(string); ok {
90		d.Name = &str
91	}
92	if str, ok := upr.Description.(string); ok {
93		d.Description = &str
94	}
95	if str, ok := upr.Purpose.(string); ok {
96		d.Purpose = &str
97	}
98	if str, ok := upr.Environment.(string); ok {
99		d.Environment = &str
100	}
101	if val, ok := upr.IsDefault.(bool); ok {
102		d.IsDefault = &val
103	}
104
105	return json.Marshal(d)
106}
107
108type assignResourcesRequest struct {
109	Resources []string `json:"resources"`
110}
111
112// ProjectResource is the projects API's representation of a resource.
113type ProjectResource struct {
114	URN        string                `json:"urn"`
115	AssignedAt string                `json:"assigned_at"`
116	Links      *ProjectResourceLinks `json:"links"`
117	Status     string                `json:"status,omitempty"`
118}
119
120// ProjectResourceLinks specify the link for more information about the resource.
121type ProjectResourceLinks struct {
122	Self string `json:"self"`
123}
124
125type projectsRoot struct {
126	Projects []Project `json:"projects"`
127	Links    *Links    `json:"links"`
128	Meta     *Meta     `json:"meta"`
129}
130
131type projectRoot struct {
132	Project *Project `json:"project"`
133}
134
135type projectResourcesRoot struct {
136	Resources []ProjectResource `json:"resources"`
137	Links     *Links            `json:"links,omitempty"`
138	Meta      *Meta             `json:"meta"`
139}
140
141var _ ProjectsService = &ProjectsServiceOp{}
142
143// List Projects.
144func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) {
145	path, err := addOptions(projectsBasePath, opts)
146	if err != nil {
147		return nil, nil, err
148	}
149
150	req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
151	if err != nil {
152		return nil, nil, err
153	}
154
155	root := new(projectsRoot)
156	resp, err := p.client.Do(ctx, req, root)
157	if err != nil {
158		return nil, resp, err
159	}
160	if l := root.Links; l != nil {
161		resp.Links = l
162	}
163	if m := root.Meta; m != nil {
164		resp.Meta = m
165	}
166
167	return root.Projects, resp, err
168}
169
170// GetDefault project.
171func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) {
172	return p.getHelper(ctx, "default")
173}
174
175// Get retrieves a single project by its ID.
176func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) {
177	return p.getHelper(ctx, projectID)
178}
179
180// Create a new project.
181func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) {
182	req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr)
183	if err != nil {
184		return nil, nil, err
185	}
186
187	root := new(projectRoot)
188	resp, err := p.client.Do(ctx, req, root)
189	if err != nil {
190		return nil, resp, err
191	}
192
193	return root.Project, resp, err
194}
195
196// Update an existing project.
197func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) {
198	path := path.Join(projectsBasePath, projectID)
199	req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur)
200	if err != nil {
201		return nil, nil, err
202	}
203
204	root := new(projectRoot)
205	resp, err := p.client.Do(ctx, req, root)
206	if err != nil {
207		return nil, resp, err
208	}
209
210	return root.Project, resp, err
211}
212
213// Delete an existing project. You cannot have any resources in a project
214// before deleting it. See the API documentation for more details.
215func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) {
216	path := path.Join(projectsBasePath, projectID)
217	req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil)
218	if err != nil {
219		return nil, err
220	}
221
222	return p.client.Do(ctx, req, nil)
223}
224
225// ListResources lists all resources in a project.
226func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) {
227	basePath := path.Join(projectsBasePath, projectID, "resources")
228	path, err := addOptions(basePath, opts)
229	if err != nil {
230		return nil, nil, err
231	}
232
233	req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
234	if err != nil {
235		return nil, nil, err
236	}
237
238	root := new(projectResourcesRoot)
239	resp, err := p.client.Do(ctx, req, root)
240	if err != nil {
241		return nil, resp, err
242	}
243	if l := root.Links; l != nil {
244		resp.Links = l
245	}
246	if m := root.Meta; m != nil {
247		resp.Meta = m
248	}
249
250	return root.Resources, resp, err
251}
252
253// AssignResources assigns one or more resources to a project. AssignResources
254// accepts resources in two possible formats:
255//  1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}`
256//  2. A valid DO URN as a string, like "do:droplet:1234"
257//
258// There is no unassign. To move a resource to another project, just assign
259// it to that other project.
260func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) {
261	path := path.Join(projectsBasePath, projectID, "resources")
262
263	ar := &assignResourcesRequest{
264		Resources: make([]string, len(resources)),
265	}
266
267	for i, resource := range resources {
268		switch resource := resource.(type) {
269		case ResourceWithURN:
270			ar.Resources[i] = resource.URN()
271		case string:
272			ar.Resources[i] = resource
273		default:
274			return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource)
275		}
276	}
277	req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar)
278	if err != nil {
279		return nil, nil, err
280	}
281
282	root := new(projectResourcesRoot)
283	resp, err := p.client.Do(ctx, req, root)
284	if err != nil {
285		return nil, resp, err
286	}
287	if l := root.Links; l != nil {
288		resp.Links = l
289	}
290
291	return root.Resources, resp, err
292}
293
294func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) {
295	path := path.Join(projectsBasePath, projectID)
296
297	req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
298	if err != nil {
299		return nil, nil, err
300	}
301
302	root := new(projectRoot)
303	resp, err := p.client.Do(ctx, req, root)
304	if err != nil {
305		return nil, resp, err
306	}
307
308	return root.Project, resp, err
309}
310