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