diff --git a/VERSION b/VERSION index f477849..373f8c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2 \ No newline at end of file +0.2.3 \ No newline at end of file diff --git a/confluence/client.go b/confluence/client.go index 8da16cf..25b42c7 100644 --- a/confluence/client.go +++ b/confluence/client.go @@ -66,7 +66,7 @@ func NewClient(input *NewClientInput) *Client { baseURL.User = url.UserPassword(input.user, input.token) return &Client{ client: &http.Client{ - Timeout: time.Second * 10, + Timeout: time.Second * 30, }, baseURL: &baseURL, basePath: basePath, diff --git a/confluence/content.go b/confluence/content.go index 0c06439..682e7db 100644 --- a/confluence/content.go +++ b/confluence/content.go @@ -27,6 +27,40 @@ type ContentLinks struct { WebUI string `json:"webui,omitempty"` } +type Page struct { + Id string `json:"id,omitempty"` + SpaceId string `json:"spaceId,omitempty"` + AuthorId string `json:"authorId,omitempty"` + OwnerId string `json:"ownerId,omitempty"` + ParentId string `json:"parentId,omitempty"` + ParentType string `json:"parentType,omitempty"` + Title string `json:"title,omitempty"` + Version *Version `json:"version,omitempty"` + Body *Body `json:"body,omitempty"` + Links *PageLinks `json:"_links,omitempty"` +} + +type PageLinks struct { + EditUI string `json:"editui,omitempty"` + TinyUI string `json:"tinyui,omitempty"` + WebUI string `json:"webui,omitempty"` +} + +type PageSearchParams struct { + Id string + SpaceID string + Title string +} + +type PageSearchResponse struct { + Results []Page `json:"results,omitempty"` + Links PageSearchResponseLinks `json:"_links,omitempty"` +} + +type PageSearchResponseLinks struct { + Next string `json:"next,omitempty"` +} + // SpaceKey is part of Content type SpaceKey struct { Key string `json:"key,omitempty"` @@ -60,6 +94,57 @@ func (c *Client) GetContent(id string) (*Content, error) { return &response, nil } +func (c *Client) GetPage(id string) (*Page, error) { + url := fmt.Sprintf("/api/v2/pages/%s?body-format=storage", id) + + var response Page + if err := c.Get(url, &response); err != nil { + return nil, err + } + return &response, nil +} + +func (c *Client) SearchPages(searchParams PageSearchParams, includeBody bool) ([]Page, error) { + + if searchParams.Id != "" { + response, err := c.GetPage(searchParams.Id) + if err != nil { + return nil, err + } + return []Page{*response}, nil + } + + url := "/api/v2/pages?limit=250" + if includeBody { + url += "&body-format=storage" + } + if searchParams.SpaceID != "" { + url += fmt.Sprintf("&space-id=%s", searchParams.SpaceID) + } + if searchParams.Title != "" { + url += fmt.Sprintf("&title=%s", searchParams.Title) + } + + var response PageSearchResponse + var result []Page + if err := c.Get(url, &response); err != nil { + return nil, err + } + + for i := 0; i < 500; i++ { + result = append(result, response.Results...) + if response.Links.Next == "" { + break + } + // Strip "/wiki" prefix from the provided URL. + nextPageUrl := response.Links.Next[5:len(response.Links.Next)] + if err := c.Get(nextPageUrl, &response); err != nil { + return nil, err + } + } + return result, nil +} + func (c *Client) UpdateContent(content *Content) (*Content, error) { var response Content content.Version.Number++ diff --git a/confluence/data_source_page.go b/confluence/data_source_page.go new file mode 100644 index 0000000..47540ab --- /dev/null +++ b/confluence/data_source_page.go @@ -0,0 +1,111 @@ +package confluence + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourcePage() *schema.Resource { + return &schema.Resource{ + Read: dataSourcePageRead, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "space_id": { + Type: schema.TypeString, + Optional: true, + }, + "title": { + Type: schema.TypeString, + Optional: true, + }, + "parent_id": { + Type: schema.TypeString, + Computed: true, + }, + "author_id": { + Type: schema.TypeString, + Computed: true, + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "parent_type": { + Type: schema.TypeString, + Computed: true, + }, + "body": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Computed: true, + }, + "view_url": { + Type: schema.TypeString, + Computed: true, + }, + "edit_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourcePageRead(d *schema.ResourceData, m interface{}) error { + client := m.(*Client) + params := PageSearchParams{} + if pageId, ok := d.Get("id").(string); ok { + params.Id = pageId + } + if spaceId, ok := d.Get("space_id").(string); ok { + params.SpaceID = spaceId + } + if title, ok := d.Get("title").(string); ok { + params.Title = title + } + + pageResponse, err := client.SearchPages(params, true) + if err != nil { + d.SetId("") + return err + } + + pageCount := len(pageResponse) + if pageCount < 1 { + return fmt.Errorf("unable to find page") + } + if pageCount > 1 { + return fmt.Errorf("found multiple pages, provide a unique arguments or use the plural data source") + } + + page := pageResponse[0] + d.SetId(page.Id) + pageMap := map[string]interface{}{ + "id": page.Id, + "space_id": page.SpaceId, + "title": page.Title, + "parent_id": page.ParentId, + "parent_type": page.ParentType, + "author_id": page.AuthorId, + "owner_id": page.OwnerId, + "body": page.Body.Storage.Value, + "version": page.Version.Number, + "view_url": client.URL(page.Links.WebUI), + "edit_url": client.URL(page.Links.EditUI), + } + + for k, v := range pageMap { + err := d.Set(k, v) + if err != nil { + return err + } + } + return nil +} diff --git a/confluence/data_source_pages.go b/confluence/data_source_pages.go new file mode 100644 index 0000000..07d07cd --- /dev/null +++ b/confluence/data_source_pages.go @@ -0,0 +1,115 @@ +package confluence + +import ( + "fmt" + "hash/crc32" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourcePages() *schema.Resource { + return &schema.Resource{ + Read: dataSourcePagesRead, + Schema: map[string]*schema.Schema{ + "space_id": { + Type: schema.TypeString, + Optional: true, + }, + "title": { + Type: schema.TypeString, + Optional: true, + }, + "pages": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "space_id": { + Type: schema.TypeString, + Computed: true, + }, + "title": { + Type: schema.TypeString, + Computed: true, + }, + "parent_id": { + Type: schema.TypeString, + Computed: true, + }, + "author_id": { + Type: schema.TypeString, + Computed: true, + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "parent_type": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Computed: true, + }, + "view_url": { + Type: schema.TypeString, + Computed: true, + }, + "edit_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourcePagesRead(d *schema.ResourceData, m interface{}) error { + client := m.(*Client) + params := PageSearchParams{} + var idStr string + if spaceId, ok := d.Get("space_id").(string); ok { + params.SpaceID = spaceId + idStr += spaceId + } + if title, ok := d.Get("title").(string); ok { + params.Title = title + idStr += title + } + + pageResponse, err := client.SearchPages(params, false) + if err != nil { + d.SetId("") + return err + } + + pages := make([]map[string]interface{}, len(pageResponse)) + for i := range pageResponse { + page := map[string]interface{}{ + "id": pageResponse[i].Id, + "space_id": pageResponse[i].SpaceId, + "title": pageResponse[i].Title, + "parent_id": pageResponse[i].ParentId, + "parent_type": pageResponse[i].ParentType, + "author_id": pageResponse[i].AuthorId, + "owner_id": pageResponse[i].OwnerId, + "version": pageResponse[i].Version.Number, + "view_url": client.URL(pageResponse[i].Links.WebUI), + "edit_url": client.URL(pageResponse[i].Links.EditUI), + } + + pages[i] = page + } + + d.SetId(fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(idStr)))) + _ = d.Set("pages", pages) + + return nil +} diff --git a/confluence/data_source_space.go b/confluence/data_source_space.go new file mode 100644 index 0000000..08f3f30 --- /dev/null +++ b/confluence/data_source_space.go @@ -0,0 +1,86 @@ +package confluence + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceSpace() *schema.Resource { + return &schema.Resource{ + Read: dataSourceSpaceRead, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "key": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "author_id": { + Type: schema.TypeString, + Computed: true, + }, + "homepage_id": { + Type: schema.TypeString, + Computed: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceSpaceRead(d *schema.ResourceData, m interface{}) error { + client := m.(*Client) + spaceKeys := []string{d.Get("key").(string)} + spaceResponse, err := client.SearchSpaces(spaceKeys) + if err != nil { + d.SetId("") + return fmt.Errorf("failed to find space: %v", err) + } + + spaceCount := len(spaceResponse) + if spaceCount < 1 { + return fmt.Errorf("space with key '%s' does not exist", spaceKeys[0]) + } + if spaceCount > 1 { + return fmt.Errorf("found multiple spaces with key '%s', provide a unique key or use the plural data source", spaceKeys[0]) + } + + space := spaceResponse[0] + d.SetId(space.Id) + spaceMap := map[string]interface{}{ + "key": space.Key, + "name": space.Name, + "description": space.Description, + "type": space.Type, + "author_id": space.AuthorId, + "homepage_id": space.HomepageId, + "url": client.URL(space.Links.WebUI), + } + + for k, v := range spaceMap { + err := d.Set(k, v) + if err != nil { + return err + } + } + + return nil +} diff --git a/confluence/data_source_spaces.go b/confluence/data_source_spaces.go new file mode 100644 index 0000000..17445fa --- /dev/null +++ b/confluence/data_source_spaces.go @@ -0,0 +1,91 @@ +package confluence + +import ( + "fmt" + "hash/crc32" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceSpaces() *schema.Resource { + return &schema.Resource{ + Read: dataSourceSpacesRead, + Schema: map[string]*schema.Schema{ + "keys": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "spaces": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "key": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "author_id": { + Type: schema.TypeString, + Computed: true, + }, + "homepage_id": { + Type: schema.TypeString, + Computed: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceSpacesRead(d *schema.ResourceData, m interface{}) error { + client := m.(*Client) + spaceKeys := interfaceSetToStringSlice(d.Get("keys")) + spaceResponse, err := client.SearchSpaces(spaceKeys) + if err != nil { + d.SetId("") + return err + } + + d.SetId(fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(strings.Join(spaceKeys, ","))))) + spaces := make([]map[string]interface{}, len(spaceResponse)) + for i := range spaceResponse { + space := map[string]interface{}{ + "id": spaceResponse[i].Id, + "key": spaceResponse[i].Key, + "name": spaceResponse[i].Name, + "description": spaceResponse[i].Description, + "type": spaceResponse[i].Type, + "author_id": spaceResponse[i].AuthorId, + "homepage_id": spaceResponse[i].HomepageId, + "url": client.URL(spaceResponse[i].Links.WebUI), + } + + spaces[i] = space + } + _ = d.Set("spaces", spaces) + return nil +} diff --git a/confluence/provider.go b/confluence/provider.go index e08946f..2214cc2 100644 --- a/confluence/provider.go +++ b/confluence/provider.go @@ -51,7 +51,12 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("CONFLUENCE_TOKEN", nil), }, }, - DataSourcesMap: map[string]*schema.Resource{}, + DataSourcesMap: map[string]*schema.Resource{ + "confluence_page": dataSourcePage(), + "confluence_pages": dataSourcePages(), + "confluence_space": dataSourceSpace(), + "confluence_spaces": dataSourceSpaces(), + }, ResourcesMap: map[string]*schema.Resource{ "confluence_content": resourceContent(), "confluence_attachment": resourceAttachment(), diff --git a/confluence/space.go b/confluence/space.go index 1e1adb3..0ec63d4 100644 --- a/confluence/space.go +++ b/confluence/space.go @@ -3,6 +3,8 @@ package confluence import ( "fmt" "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // Content is a primary resource in Confluence @@ -19,6 +21,31 @@ type SpaceLinks struct { WebUI string `json:"webui,omitempty"` } +// The space schema from API v2, note the id datatype differs from the v1 API. +type SpaceV2 struct { + Id string `json:"id,omitempty"` + AuthorId string `json:"authorId,omitempty"` + HomepageId string `json:"homepageId,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Key string `json:"key,omitempty"` + Links *SpaceV2Links `json:"_links,omitempty"` +} + +type SpaceV2Links struct { + WebUI string `json:"webui,omitempty"` +} + +type SpaceSearchResults struct { + Results []SpaceV2 `json:"results,omitempty"` + Links SpaceSearchResponseLinks `json:"_links,omitempty"` +} + +type SpaceSearchResponseLinks struct { + Next string `json:"next,omitempty"` +} + func (c *Client) CreateSpace(space *Space) (*Space, error) { var response Space if err := c.Post("/rest/api/space", space, &response); err != nil { @@ -37,6 +64,29 @@ func (c *Client) GetSpace(id string) (*Space, error) { return &response, nil } +func (c *Client) SearchSpaces(keys []string) ([]SpaceV2, error) { + var response SpaceSearchResults + var result []SpaceV2 + path := fmt.Sprintf("/api/v2/spaces?limit=250&keys=%s", strings.Join(keys, ",")) + if err := c.Get(path, &response); err != nil { + return nil, err + } + + for i := 0; i < 500; i++ { + result = append(result, response.Results...) + if response.Links.Next == "" { + break + } + // Strip "/wiki" prefix from the provided URL. + nextPageUrl := response.Links.Next[5:len(response.Links.Next)] + if err := c.Get(nextPageUrl, &response); err != nil { + return nil, err + } + } + + return result, nil +} + func (c *Client) UpdateSpace(space *Space) (*Space, error) { var response Space @@ -59,3 +109,16 @@ func (c *Client) DeleteSpace(id string) error { } return nil } + +// Convert a resource string set to a native slice. +func interfaceSetToStringSlice(ifSet interface{}) []string { + var strSlice []string + ifList := ifSet.(*schema.Set).List() + strSlice = make([]string, len(ifList)) + for i, rawVal := range ifList { + if strVal, ok := rawVal.(string); ok { + strSlice[i] = strVal + } + } + return strSlice +} diff --git a/docs/data-sources/confluence_page.md b/docs/data-sources/confluence_page.md new file mode 100644 index 0000000..c8c3056 --- /dev/null +++ b/docs/data-sources/confluence_page.md @@ -0,0 +1,60 @@ +--- +layout: "confluence" +page_title: "Data Source: confluence_page" +sidebar_current: "docs-confluence-data-page" +description: |- + Get a Confluence page +--- + +# Data Source: confluence_page + +Retrieve details about an existing Confluence page. + +## Example Usage + +```hcl +data confluence_space "my_space" { + key = "MYSPACE" +} + +data confluence_page "my_page" { + space_id = data.confluence_space.my_space.id + title = "my page" +} +``` + +## Argument Reference + +The following arguments are supported: + +- `id` - (Optional) The target Confluence page's ID. + +- `space_id` - (Optional) The ID of the space containing the target Confluence page. + +- `title` - (Optional) The target Confluence page's title. + +## Attributes Reference + +This resource exports the following attributes: + +- `id` - (Optional) This page's ID. + +- `space_id` - (Optional) The ID of the space containing this page. + +- `title` - (Optional) This page's title. + +- `body` - The actual content of the page in Confluence Storage Format. + +- `parent_id` - The content id of the page's parent. + +- `parent_type` - The content type of the page's parent. + +- `author_id` - The ID of the page's most recent author. + +- `owner_id` - The ID of the page's owner. + +- `view_url` - The URL to view this page. + +- `edit_url` - The URL to edit this page. + +- `version` - The version of this page. diff --git a/docs/data-sources/confluence_pages.md b/docs/data-sources/confluence_pages.md new file mode 100644 index 0000000..0f29950 --- /dev/null +++ b/docs/data-sources/confluence_pages.md @@ -0,0 +1,56 @@ +--- +layout: "confluence" +page_title: "Data Source: confluence_pages" +sidebar_current: "docs-confluence-data-pages" +description: |- + Get zero or more Confluence pages +--- + +# Data Source: confluence_pages + +Retrieve details about zero or more existing Confluence pages. + +## Example Usage + +```hcl +data confluence_space "my_space" { + key = "MYSPACE" +} + +data confluence_pages "my_space_pages" { + space_id = data.confluence_space.my_space.id +} +``` + +## Argument Reference + +The following arguments are supported: + +- `space_id` - (Optional) The ID of the space containing the target Confluence page. +- `title` - (Optional) The target Confluence page's title. + +## Attributes Reference + +This resource exports the following attributes: + +- `pages` - The list of pages that match the provided arguments, each with the following attributes: + + - `id` - (Optional) This page's ID. + + - `space_id` - (Optional) The ID of the space containing this page. + + - `title` - (Optional) This page's title. + + - `parent_id` - The content id of the page's parent. + + - `parent_type` - The content type of the page's parent. + + - `author_id` - The ID of the page's most recent author. + + - `owner_id` - The ID of the page's owner. + + - `view_url` - The URL to view this page. + + - `edit_url` - The URL to edit this page. + + - `version` - The version of this page. diff --git a/docs/data-sources/confluence_space.md b/docs/data-sources/confluence_space.md new file mode 100644 index 0000000..31070d6 --- /dev/null +++ b/docs/data-sources/confluence_space.md @@ -0,0 +1,43 @@ +--- +layout: "confluence" +page_title: "Data Source: confluence_space" +sidebar_current: "docs-confluence-data-space" +description: |- + Get a Confluence space +--- + +# Data Source: confluence_space + +Retrieve details about an existing Confluence space. + +## Example Usage + +```hcl +data confluence_space "my_space" { + key = "MYSPACE" +} +``` + +## Argument Reference + +The following arguments are supported: + +- `key` - (Required) The key of the target space. + +## Attribute Reference + +The following attributes are provided: + +- `id` - The space's id. + +- `key` - The space's key. + +- `name` - The space's name. + +- `type` - The space's type. + +- `description` - The space's description. + +- `author_id` - The ID of the space author. + +- `url` - The space's URL. diff --git a/docs/data-sources/confluence_spaces.md b/docs/data-sources/confluence_spaces.md new file mode 100644 index 0000000..2435038 --- /dev/null +++ b/docs/data-sources/confluence_spaces.md @@ -0,0 +1,44 @@ +--- +layout: "confluence" +page_title: "Data Source: confluence_spaces" +sidebar_current: "docs-confluence-data-spaces" +description: |- + Get zero or more Confluence spaces +--- + +# Data Source: confluence_spaces + +Retrieve details about zero or more existing Confluence spaces. + +## Example Usage + +```hcl +data confluence_spaces "all_spaces" { +} +``` + +## Argument Reference + +The following arguments are supported: + +- `keys` - (Optional) A list of space keys to retrieve, when omitted all spaces will be returned. + +## Attribute Reference + +The following attributes are provided: + +- `spaces` - The list of spaces that match the provided arguments, each with the following attributes: + + - `id` - The space's id. + + - `key` - The space's key. + + - `name` - The space's name. + + - `type` - The space's type. + + - `description` - The space's description. + + - `author_id` - The ID of the space author. + + - `url` - The space's URL.