mirror of
https://github.com/ysoftdevs/terraform-provider-bitbucketserver.git
synced 2026-03-25 19:01:06 +01:00
feat: Support repository access tokens
This is very much a WIP. It will be extended quite a bit to support automatic recreation and similar stuff. For now it will only be pushed to be used internally.
This commit is contained in:
212
bitbucket/resource_repository_access_token.go
Normal file
212
bitbucket/resource_repository_access_token.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/xvlcwk-terraform/terraform-provider-bitbucketserver/bitbucket/util"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type repositoryAccessTokenModel struct {
|
||||
Id types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Permissions types.Set `tfsdk:"permissions"`
|
||||
ExpireIn types.Int64 `tfsdk:"expire_in"`
|
||||
Project types.String `tfsdk:"project"`
|
||||
Repository types.String `tfsdk:"repository"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
CreatedDate types.Int64 `tfsdk:"created_date"`
|
||||
}
|
||||
|
||||
type repositoryAccessTokenResource struct {
|
||||
resourceHelper *util.AccessTokenResourceHelper
|
||||
}
|
||||
|
||||
func newRepositoryAccessTokenResource() resource.Resource {
|
||||
return &repositoryAccessTokenResource{
|
||||
resourceHelper: util.NewAccessTokenResourceHelper(),
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the implementation satisfies the desired interfaces.
|
||||
var _ resource.ResourceWithConfigure = &repositoryAccessTokenResource{}
|
||||
|
||||
// Metadata should return the full name of the resource.
|
||||
func (r *repositoryAccessTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_repository_access_token"
|
||||
}
|
||||
|
||||
// Schema should return the schema for this resource.
|
||||
func (r *repositoryAccessTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "An HTTP-Access token limited to the given repository",
|
||||
Attributes: r.resourceHelper.Schema(map[string]schema.Attribute{
|
||||
"repository": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The repository slug",
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data repositoryAccessTokenModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload, diagnostics := r.createRequestData(ctx, data)
|
||||
|
||||
if diagnostics != nil {
|
||||
resp.Diagnostics.Append(diagnostics...)
|
||||
}
|
||||
|
||||
tokenResponse, tokenErrorResponse := r.resourceHelper.Client.Put(r.getUrlForProject(data), bytes.NewBuffer(payload))
|
||||
response, convertingResponseDiagnostics := r.readResponse(tokenErrorResponse, tokenResponse, &data)
|
||||
if convertingResponseDiagnostics != nil {
|
||||
resp.Diagnostics.Append(convertingResponseDiagnostics)
|
||||
return
|
||||
}
|
||||
|
||||
data.Token = types.StringValue(response.Token)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
func (r *repositoryAccessTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data repositoryAccessTokenModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
tokenResponse, tokenErrorResponse := r.resourceHelper.Client.Get(r.getUrlForId(data))
|
||||
_, diagnostic := r.readResponse(tokenErrorResponse, tokenResponse, &data)
|
||||
if diagnostic != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data repositoryAccessTokenModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload, diagnostics := r.createRequestData(ctx, data)
|
||||
|
||||
if diagnostics != nil {
|
||||
resp.Diagnostics.Append(diagnostics...)
|
||||
}
|
||||
tokenResponse, tokenErrorResponse := r.resourceHelper.Client.Post(r.getUrlForId(data), bytes.NewBuffer(payload))
|
||||
_, convertingResponseDiagnostics := r.readResponse(tokenErrorResponse, tokenResponse, &data)
|
||||
if convertingResponseDiagnostics != nil {
|
||||
resp.Diagnostics.Append(convertingResponseDiagnostics)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data repositoryAccessTokenModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
tokenResponse, tokenErrorResponse := r.resourceHelper.Client.Delete(r.getUrlForId(data))
|
||||
if tokenErrorResponse != nil {
|
||||
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
|
||||
"Unable to Delete Resource",
|
||||
"An unexpected error occurred while deleting the resource. "+
|
||||
"Please report this issue to the provider developers.\n\n"+
|
||||
"Error: "+tokenErrorResponse.Error()))
|
||||
}
|
||||
|
||||
if tokenResponse.StatusCode != 204 {
|
||||
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
|
||||
"Unable to Delete Resource",
|
||||
"An unexpected statusCode occurred while deleting the resource. "+
|
||||
"Please report this issue to the provider developers.\n\n"+
|
||||
"Status: "+tokenResponse.Status))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) Configure(ctx context.Context, configureRequest resource.ConfigureRequest, configureResponse *resource.ConfigureResponse) {
|
||||
r.resourceHelper.Configure(ctx, configureRequest, configureResponse)
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) createRequestData(ctx context.Context, data repositoryAccessTokenModel) ([]byte, diag.Diagnostics) {
|
||||
var permissions []string
|
||||
permissionConversionDiagnostics := data.Permissions.ElementsAs(ctx, &permissions, false)
|
||||
if permissionConversionDiagnostics != nil {
|
||||
return nil, permissionConversionDiagnostics
|
||||
}
|
||||
tokenRequest := &util.CreateAccessTokenRequest{
|
||||
ExpiryDays: data.ExpireIn.ValueInt64(),
|
||||
Name: data.Name.ValueString(),
|
||||
Permissions: permissions,
|
||||
}
|
||||
payload, jsonEncodingError := json.Marshal(tokenRequest)
|
||||
if jsonEncodingError != nil {
|
||||
return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Marshalling error", fmt.Sprintf("Failed to encode %v. Is it valid Json?", tokenRequest))}
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) readResponse(tokenErrorResponse error, tokenResponse *http.Response, data *repositoryAccessTokenModel) (*util.AccessTokenResponse, *diag.ErrorDiagnostic) {
|
||||
if tokenErrorResponse != nil {
|
||||
diagnostic := diag.NewErrorDiagnostic("http error", tokenErrorResponse.Error())
|
||||
return nil, &diagnostic
|
||||
}
|
||||
if tokenResponse.StatusCode != 200 {
|
||||
diagnostic := diag.NewErrorDiagnostic("http error", fmt.Sprintf("Response Status: %d", tokenResponse.StatusCode))
|
||||
return nil, &diagnostic
|
||||
}
|
||||
|
||||
body, readBodyError := io.ReadAll(tokenResponse.Body)
|
||||
if readBodyError != nil {
|
||||
diagnostic := diag.NewErrorDiagnostic("Error reading response", "Failed to read response. Is it valid Json?")
|
||||
return nil, &diagnostic
|
||||
}
|
||||
|
||||
response := &util.AccessTokenResponse{}
|
||||
unmarshallError := json.Unmarshal(body, response)
|
||||
if unmarshallError != nil {
|
||||
diagnostic := diag.NewErrorDiagnostic("Error reading response", fmt.Sprintf("Failed to read response %v. Is it valid Json?, %v", string(body), unmarshallError))
|
||||
return nil, &diagnostic
|
||||
}
|
||||
|
||||
data.Id = types.StringValue(response.Id)
|
||||
data.CreatedDate = types.Int64Value(response.CreatedDate)
|
||||
data.Name = types.StringValue(response.Name)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) getUrlForId(data repositoryAccessTokenModel) string {
|
||||
return fmt.Sprintf("%v/%v", r.getUrlForProject(data), data.Id.ValueString())
|
||||
}
|
||||
|
||||
func (r *repositoryAccessTokenResource) getUrlForProject(data repositoryAccessTokenModel) string {
|
||||
projectKey := data.Project.ValueString()
|
||||
repositorySlug := data.Repository.ValueString()
|
||||
repositoryUrl := fmt.Sprintf("/rest/access-tokens/latest/projects/%v/repos/%v", projectKey, repositorySlug)
|
||||
return repositoryUrl
|
||||
}
|
||||
51
bitbucket/resource_repository_access_token_test.go
Normal file
51
bitbucket/resource_repository_access_token_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package bitbucket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccBitbucketResourceRepositoryAccessToken(t *testing.T) {
|
||||
projectKey := fmt.Sprintf("TEST%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int())
|
||||
|
||||
config := fmt.Sprintf(`
|
||||
resource "bitbucketserver_project" "test" {
|
||||
key = "%v"
|
||||
name = "test-project-%v"
|
||||
}
|
||||
|
||||
resource "bitbucketserver_repository" "test" {
|
||||
project = bitbucketserver_project.test.key
|
||||
name = "repo"
|
||||
}
|
||||
|
||||
resource "bitbucketserver_repository_access_token" "test" {
|
||||
project = bitbucketserver_project.test.key
|
||||
repository = bitbucketserver_repository.test.slug
|
||||
name = "newLabelForTest"
|
||||
permissions = ["REPO_READ"]
|
||||
}
|
||||
`, projectKey, projectKey)
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
ProtoV6ProviderFactories: ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: config,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("bitbucketserver_repository_access_token.test", "id"),
|
||||
resource.TestCheckResourceAttr("bitbucketserver_repository_access_token.test", "project", projectKey),
|
||||
resource.TestCheckResourceAttr("bitbucketserver_repository_access_token.test", "repository", "repo"),
|
||||
resource.TestCheckResourceAttr("bitbucketserver_repository_access_token.test", "name", "newLabelForTest"),
|
||||
resource.TestCheckResourceAttr("bitbucketserver_repository_access_token.test", "permissions.#", "1"),
|
||||
resource.TestCheckResourceAttr("bitbucketserver_repository_access_token.test", "permissions.0", "REPO_READ"),
|
||||
resource.TestCheckResourceAttrSet("bitbucketserver_repository_access_token.test", "token"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
98
bitbucket/util/access_token_resource_helper.go
Normal file
98
bitbucket/util/access_token_resource_helper.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/xvlcwk-terraform/terraform-provider-bitbucketserver/bitbucket/util/client"
|
||||
)
|
||||
|
||||
type (
|
||||
// AccessTokenResourceHelper provides assistive snippets of logic to help reduce duplication in
|
||||
// each resource definition.
|
||||
AccessTokenResourceHelper struct {
|
||||
Client *client.BitbucketClient
|
||||
helper *ResourceHelper
|
||||
}
|
||||
)
|
||||
|
||||
func NewAccessTokenResourceHelper() *AccessTokenResourceHelper {
|
||||
return &AccessTokenResourceHelper{
|
||||
helper: NewResourceHelper(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AccessTokenResourceHelper) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
r.helper.Configure(ctx, req, resp)
|
||||
r.Client = r.helper.client
|
||||
}
|
||||
|
||||
func (r *AccessTokenResourceHelper) Schema(s map[string]schema.Attribute) map[string]schema.Attribute {
|
||||
s = r.helper.Schema(s)
|
||||
if _, ok := s["id"]; !ok {
|
||||
s["id"] = schema.StringAttribute{
|
||||
Computed: true,
|
||||
MarkdownDescription: "The id of the access token",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if _, ok := s["name"]; !ok {
|
||||
s["name"] = schema.StringAttribute{
|
||||
Required: true,
|
||||
MarkdownDescription: "The label for the access token",
|
||||
}
|
||||
}
|
||||
if _, ok := s["project"]; !ok {
|
||||
s["project"] = schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "The project slug",
|
||||
}
|
||||
}
|
||||
if _, ok := s["token"]; !ok {
|
||||
s["token"] = schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "The token. Only set if created by Terraform",
|
||||
}
|
||||
}
|
||||
if _, ok := s["permissions"]; !ok {
|
||||
s["permissions"] = schema.SetAttribute{
|
||||
Required: true,
|
||||
Description: fmt.Sprintf("The permissions this access token has for repositories."),
|
||||
ElementType: types.StringType,
|
||||
}
|
||||
}
|
||||
if _, ok := s["expire_in"]; !ok {
|
||||
s["expire_in"] = schema.Int64Attribute{
|
||||
Computed: true,
|
||||
Description: "Expire in X Days. If not set it does not expire",
|
||||
Default: int64default.StaticInt64(1095),
|
||||
}
|
||||
}
|
||||
if _, ok := s["created_date"]; !ok {
|
||||
s["created_date"] = schema.Int64Attribute{
|
||||
Computed: true,
|
||||
Description: "Created Date",
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type CreateAccessTokenRequest struct {
|
||||
ExpiryDays int64 `json:"expiryDays" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Permissions []string `json:"permissions" binding:"required"`
|
||||
}
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
Token string `json:"token,omitempty"` // Only available on creation
|
||||
Name string `json:"name" binding:"required"`
|
||||
Id string `json:"id" binding:"required"`
|
||||
CreatedDate int64 `json:"createdDate" binding:"required"`
|
||||
}
|
||||
45
bitbucket/util/resource_helper.go
Normal file
45
bitbucket/util/resource_helper.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
client2 "github.com/xvlcwk-terraform/terraform-provider-bitbucketserver/bitbucket/util/client"
|
||||
bitbucketTypes "github.com/xvlcwk-terraform/terraform-provider-bitbucketserver/bitbucket/util/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// ResourceHelper provides assistive snippets of logic to help reduce duplication in
|
||||
// each resource definition.
|
||||
ResourceHelper struct {
|
||||
client *client2.BitbucketClient
|
||||
}
|
||||
)
|
||||
|
||||
func NewResourceHelper() *ResourceHelper {
|
||||
return &ResourceHelper{}
|
||||
}
|
||||
|
||||
// Configure should register the client for the resource.
|
||||
func (r *ResourceHelper) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
provider, ok := req.ProviderData.(*bitbucketTypes.BitbucketServerProvider)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *bitbucket.BitbucketServerProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = provider.BitbucketClient
|
||||
}
|
||||
|
||||
func (r *ResourceHelper) Schema(s map[string]schema.Attribute) map[string]schema.Attribute {
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user