mirror of
https://github.com/ysoftdevs/terraform-provider-bitbucket.git
synced 2026-03-30 22:21:51 +02:00
Refactoring
This commit is contained in:
@@ -73,7 +73,7 @@ func (p *bitbucketTokenProvider) Configure(ctx context.Context, req provider.Con
|
|||||||
providerData := &ProviderData{
|
providerData := &ProviderData{
|
||||||
AuthHeader: config.AuthHeader.ValueString(),
|
AuthHeader: config.AuthHeader.ValueString(),
|
||||||
ServerURL: config.ServerURL.ValueString(),
|
ServerURL: config.ServerURL.ValueString(),
|
||||||
TLSSkipVerify: config.TLSSkipVerify.ValueBool(), // <-- passes TLS flag through
|
TLSSkipVerify: config.TLSSkipVerify.ValueBool(),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.DataSourceData = providerData
|
resp.DataSourceData = providerData
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ import (
|
|||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderData contains configuration passed from the provider to the resource.
|
|
||||||
type ProviderData struct {
|
type ProviderData struct {
|
||||||
AuthHeader string
|
AuthHeader string
|
||||||
ServerURL string
|
ServerURL string
|
||||||
TLSSkipVerify bool
|
TLSSkipVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// BitbucketTokenResource manages Bitbucket repository access tokens.
|
|
||||||
type BitbucketTokenResource struct {
|
type BitbucketTokenResource struct {
|
||||||
authHeader string
|
authHeader string
|
||||||
serverURL string
|
serverURL string
|
||||||
@@ -33,23 +31,20 @@ func NewBitbucketTokenResource() resource.Resource {
|
|||||||
return &BitbucketTokenResource{}
|
return &BitbucketTokenResource{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BitbucketTokenResourceModel maps Terraform schema attributes to Go fields.
|
|
||||||
type BitbucketTokenResourceModel struct {
|
type BitbucketTokenResourceModel struct {
|
||||||
ID types.String `tfsdk:"id"`
|
ID types.String `tfsdk:"id"`
|
||||||
TokenName types.String `tfsdk:"token_name"` // prefix provided by user
|
TokenName types.String `tfsdk:"token_name"`
|
||||||
ProjectName types.String `tfsdk:"project_name"`
|
ProjectName types.String `tfsdk:"project_name"`
|
||||||
RepositoryName types.String `tfsdk:"repository_name"`
|
RepositoryName types.String `tfsdk:"repository_name"`
|
||||||
Token types.String `tfsdk:"token"` // secret; returned only on creation; preserved from state
|
Token types.String `tfsdk:"token"`
|
||||||
CurrentTokenName types.String `tfsdk:"current_token_name"` // actual token identifier (prefix-epoch)
|
CurrentTokenName types.String `tfsdk:"current_token_name"`
|
||||||
CurrentTokenExpiry types.Int64 `tfsdk:"current_token_expiry"` // ms since epoch
|
CurrentTokenExpiry types.Int64 `tfsdk:"current_token_expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata defines the Terraform resource type name.
|
|
||||||
func (r *BitbucketTokenResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
|
func (r *BitbucketTokenResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
resp.TypeName = "bitbucket_token"
|
resp.TypeName = "bitbucket_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schema defines the Terraform resource schema.
|
|
||||||
func (r *BitbucketTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
func (r *BitbucketTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
Description: "Manages Bitbucket access tokens for a repository. The token secret is only returned when created and is preserved in state for reuse while valid.",
|
Description: "Manages Bitbucket access tokens for a repository. The token secret is only returned when created and is preserved in state for reuse while valid.",
|
||||||
@@ -86,7 +81,6 @@ func (r *BitbucketTokenResource) Schema(_ context.Context, _ resource.SchemaRequ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure sets up provider-level data for the resource.
|
|
||||||
func (r *BitbucketTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
func (r *BitbucketTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
if req.ProviderData == nil {
|
if req.ProviderData == nil {
|
||||||
return
|
return
|
||||||
@@ -114,10 +108,9 @@ func (r *BitbucketTokenResource) Configure(_ context.Context, req resource.Confi
|
|||||||
r.tlsSkipVerify = providerData.TLSSkipVerify
|
r.tlsSkipVerify = providerData.TLSSkipVerify
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpClient creates a custom HTTP client with optional TLS skip verification.
|
|
||||||
func (r *BitbucketTokenResource) httpClient() *http.Client {
|
func (r *BitbucketTokenResource) httpClient() *http.Client {
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: r.tlsSkipVerify}, // #nosec G402 - intentional per user config
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: r.tlsSkipVerify},
|
||||||
}
|
}
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Timeout: 20 * time.Second,
|
Timeout: 20 * time.Second,
|
||||||
@@ -125,14 +118,12 @@ func (r *BitbucketTokenResource) httpClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenInfo describes an access token returned by listing API.
|
|
||||||
type tokenInfo struct {
|
type tokenInfo struct {
|
||||||
Name string
|
Name string
|
||||||
ExpiryMs int64
|
ExpiryMs int64
|
||||||
Permissions []string
|
Permissions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// listTokens lists all tokens for a repo and filters by prefix; returns all matches.
|
|
||||||
func (r *BitbucketTokenResource) listTokens(auth, baseURL, project, repo, prefix string) ([]tokenInfo, error) {
|
func (r *BitbucketTokenResource) listTokens(auth, baseURL, project, repo, prefix string) ([]tokenInfo, error) {
|
||||||
apiURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s?limit=10000", baseURL, project, repo)
|
apiURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s?limit=10000", baseURL, project, repo)
|
||||||
client := r.httpClient()
|
client := r.httpClient()
|
||||||
@@ -166,7 +157,7 @@ func (r *BitbucketTokenResource) listTokens(auth, baseURL, project, repo, prefix
|
|||||||
if len(name) < len(prefix) || name[:len(prefix)] != prefix {
|
if len(name) < len(prefix) || name[:len(prefix)] != prefix {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
exp, _ := obj["expiryDate"].(float64) // ms since epoch
|
exp, _ := obj["expiryDate"].(float64)
|
||||||
expMs := int64(exp)
|
expMs := int64(exp)
|
||||||
|
|
||||||
var perms []string
|
var perms []string
|
||||||
@@ -186,7 +177,6 @@ func (r *BitbucketTokenResource) listTokens(auth, baseURL, project, repo, prefix
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTokenByName searches list results for an exact name.
|
|
||||||
func getTokenByName(tokens []tokenInfo, name string) *tokenInfo {
|
func getTokenByName(tokens []tokenInfo, name string) *tokenInfo {
|
||||||
for i := range tokens {
|
for i := range tokens {
|
||||||
if tokens[i].Name == name {
|
if tokens[i].Name == name {
|
||||||
@@ -196,7 +186,6 @@ func getTokenByName(tokens []tokenInfo, name string) *tokenInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createToken creates a new access token and returns (secret, name, expiryMs).
|
|
||||||
func (r *BitbucketTokenResource) createToken(auth, baseURL, project, repo, prefix string) (string, string, int64, error) {
|
func (r *BitbucketTokenResource) createToken(auth, baseURL, project, repo, prefix string) (string, string, int64, error) {
|
||||||
putURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s", baseURL, project, repo)
|
putURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s", baseURL, project, repo)
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
@@ -238,7 +227,6 @@ func (r *BitbucketTokenResource) createToken(auth, baseURL, project, repo, prefi
|
|||||||
return secret, name, expMs, nil
|
return secret, name, expMs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteToken removes a token by name.
|
|
||||||
func (r *BitbucketTokenResource) deleteToken(auth, baseURL, project, repo, name string) error {
|
func (r *BitbucketTokenResource) deleteToken(auth, baseURL, project, repo, name string) error {
|
||||||
client := r.httpClient()
|
client := r.httpClient()
|
||||||
delURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s/%s", baseURL, project, repo, name)
|
delURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s/%s", baseURL, project, repo, name)
|
||||||
@@ -258,9 +246,6 @@ func (r *BitbucketTokenResource) deleteToken(auth, baseURL, project, repo, name
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureToken ensures we end with a valid token secret in state.
|
|
||||||
// If state has a valid token → keep its secret.
|
|
||||||
// If missing/expired → delete expired (if any) and create a fresh one.
|
|
||||||
func (r *BitbucketTokenResource) ensureToken(data *BitbucketTokenResourceModel) (*BitbucketTokenResourceModel, error) {
|
func (r *BitbucketTokenResource) ensureToken(data *BitbucketTokenResourceModel) (*BitbucketTokenResourceModel, error) {
|
||||||
project := data.ProjectName.ValueString()
|
project := data.ProjectName.ValueString()
|
||||||
repo := data.RepositoryName.ValueString()
|
repo := data.RepositoryName.ValueString()
|
||||||
@@ -308,8 +293,6 @@ func (r *BitbucketTokenResource) ensureToken(data *BitbucketTokenResourceModel)
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create — always produces a token value. Since no prior state exists,
|
|
||||||
// we create a fresh token (after cleaning up any expired ones for the prefix).
|
|
||||||
func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
var data BitbucketTokenResourceModel
|
var data BitbucketTokenResourceModel
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
@@ -334,15 +317,11 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// FIX #1: unknown values must be treated as drift
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
if data.CurrentTokenName.IsUnknown() || data.Token.IsUnknown() {
|
if data.CurrentTokenName.IsUnknown() || data.Token.IsUnknown() {
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no ID or no token name → resource is incomplete → drift
|
|
||||||
if data.ID.IsUnknown() || data.ID.IsNull() ||
|
if data.ID.IsUnknown() || data.ID.IsNull() ||
|
||||||
data.CurrentTokenName.IsNull() {
|
data.CurrentTokenName.IsNull() {
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
@@ -353,7 +332,6 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
|
|||||||
repo := data.RepositoryName.ValueString()
|
repo := data.RepositoryName.ValueString()
|
||||||
prefix := data.TokenName.ValueString()
|
prefix := data.TokenName.ValueString()
|
||||||
|
|
||||||
// List tokens from Bitbucket
|
|
||||||
tokens, err := r.listTokens(r.authHeader, r.serverURL, project, repo, prefix)
|
tokens, err := r.listTokens(r.authHeader, r.serverURL, project, repo, prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Error listing tokens", err.Error())
|
resp.Diagnostics.AddError("Error listing tokens", err.Error())
|
||||||
@@ -364,37 +342,24 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
|
|||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
thresholdMs := int64(30 * 24 * time.Hour / time.Millisecond)
|
thresholdMs := int64(30 * 24 * time.Hour / time.Millisecond)
|
||||||
|
|
||||||
// Find token in server list
|
|
||||||
t := getTokenByName(tokens, stateName)
|
t := getTokenByName(tokens, stateName)
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// FIX #2: drift if token does not exist anymore
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
if t == nil {
|
if t == nil {
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time remaining before expiration
|
|
||||||
timeLeft := t.ExpiryMs - nowMs
|
timeLeft := t.ExpiryMs - nowMs
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// FIX #3: expired or expiring soon → drift
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
if timeLeft <= thresholdMs {
|
if timeLeft <= thresholdMs {
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// All good → update expiry in state
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
data.CurrentTokenExpiry = types.Int64Value(t.ExpiryMs)
|
data.CurrentTokenExpiry = types.Int64Value(t.ExpiryMs)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update — same semantics as Create: ensure we output a valid token value.
|
|
||||||
// If state has a valid token, reuse its secret; otherwise delete expired and create new.
|
|
||||||
func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
var plan BitbucketTokenResourceModel
|
var plan BitbucketTokenResourceModel
|
||||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
@@ -402,11 +367,9 @@ func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.Update
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carry over prior state (for secret) if present.
|
|
||||||
var state BitbucketTokenResourceModel
|
var state BitbucketTokenResourceModel
|
||||||
_ = req.State.Get(ctx, &state)
|
_ = req.State.Get(ctx, &state)
|
||||||
|
|
||||||
// Start from plan but keep any state-held secret/name/expiry for reuse.
|
|
||||||
if !state.Token.IsNull() && !state.Token.IsUnknown() {
|
if !state.Token.IsNull() && !state.Token.IsUnknown() {
|
||||||
plan.Token = state.Token
|
plan.Token = state.Token
|
||||||
}
|
}
|
||||||
@@ -427,7 +390,6 @@ func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.Update
|
|||||||
resp.Diagnostics.Append(resp.State.Set(ctx, out)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, out)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the tracked token if it still exists.
|
|
||||||
func (r *BitbucketTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
func (r *BitbucketTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
var data BitbucketTokenResourceModel
|
var data BitbucketTokenResourceModel
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ func TestAccBitbucketToken_AllScenarios(t *testing.T) {
|
|||||||
PreConfig: func() {
|
PreConfig: func() {
|
||||||
server.SetExpiredToken("proj/repo")
|
server.SetExpiredToken("proj/repo")
|
||||||
},
|
},
|
||||||
RefreshState: true,
|
RefreshState: true,
|
||||||
ExpectNonEmptyPlan: true,
|
ExpectNonEmptyPlan: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -130,22 +130,6 @@ resource "bitbucket_token" "test" {
|
|||||||
`, url)
|
`, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAccBitbucketTokenConfigPrefix(url, prefix string) string {
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
provider "bitbucket" {
|
|
||||||
server_url = "%s"
|
|
||||||
auth_header = "dummy"
|
|
||||||
tls_skip_verify = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "bitbucket_token" "test" {
|
|
||||||
project_name = "proj"
|
|
||||||
repository_name = "repo"
|
|
||||||
token_name = "%s"
|
|
||||||
}
|
|
||||||
`, url, prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ---------- Environment ----------
|
// ---------- Environment ----------
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user