1 Commits

Author SHA1 Message Date
Jan Husak
30f4d9a62b generic server url 2025-11-07 10:22:13 +01:00
2 changed files with 89 additions and 47 deletions

View File

@@ -16,6 +16,11 @@ func NewProvider() provider.Provider {
type bitbucketTokenProvider struct{}
type bitbucketTokenProviderModel struct {
AuthHeader types.String `tfsdk:"auth_header"`
ServerURL types.String `tfsdk:"server_url"`
}
func (p *bitbucketTokenProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "bitbucket"
}
@@ -26,30 +31,50 @@ func (p *bitbucketTokenProvider) Schema(_ context.Context, _ provider.SchemaRequ
Attributes: map[string]schema.Attribute{
"auth_header": schema.StringAttribute{
Description: "Base64 encoded Basic Auth header or personal access token.",
Optional: true,
Sensitive: true, // <--- klíčové
Required: true,
Sensitive: true,
},
"server_url": schema.StringAttribute{
Description: "Base URL of the Bitbucket server (e.g. https://stash.example.com). Must not end with a slash.",
Required: true,
},
},
}
}
func (p *bitbucketTokenProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config struct {
AuthHeader types.String `tfsdk:"auth_header"`
}
var config bitbucketTokenProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
if config.AuthHeader.IsNull() {
resp.Diagnostics.AddWarning("Missing credentials", "No auth_header provided — provider will not authenticate requests.")
if config.ServerURL.IsNull() || config.ServerURL.ValueString() == "" {
resp.Diagnostics.AddError(
"Missing server URL",
"The provider requires a 'server_url' to be specified.",
)
return
}
resp.DataSourceData = config.AuthHeader.ValueString()
resp.ResourceData = config.AuthHeader.ValueString()
if config.AuthHeader.IsNull() || config.AuthHeader.ValueString() == "" {
resp.Diagnostics.AddError(
"Missing authentication header",
"The provider requires an 'auth_header' to be specified.",
)
return
}
serverURL := config.ServerURL.ValueString()
authHeader := config.AuthHeader.ValueString()
providerData := ProviderData{
AuthHeader: authHeader,
ServerURL: serverURL,
}
resp.DataSourceData = providerData
resp.ResourceData = providerData
}
func (p *bitbucketTokenProvider) DataSources(_ context.Context) []func() datasource.DataSource {

View File

@@ -14,8 +14,14 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
)
type ProviderData struct {
AuthHeader string
ServerURL string
}
type BitbucketTokenResource struct {
authHeader string
serverURL string
}
func NewBitbucketTokenResource() resource.Resource {
@@ -42,16 +48,21 @@ func (r *BitbucketTokenResource) Schema(_ context.Context, req resource.SchemaRe
Computed: true,
},
"token_name": schema.StringAttribute{
Required: true,
Description: "Name prefix for the Bitbucket access token.",
Required: true,
},
"project_name": schema.StringAttribute{
Required: true,
Description: "Name of the Bitbucket project.",
Required: true,
},
"repository_name": schema.StringAttribute{
Required: true,
Description: "Name of the Bitbucket repository.",
Required: true,
},
"token": schema.StringAttribute{
Computed: true,
Description: "Generated Bitbucket access token (sensitive).",
Computed: true,
Sensitive: true,
},
},
}
@@ -59,22 +70,36 @@ func (r *BitbucketTokenResource) Schema(_ context.Context, req resource.SchemaRe
func (r *BitbucketTokenResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
auth, ok := req.ProviderData.(string)
if !ok {
resp.Diagnostics.AddError(
"Unexpected provider data type",
fmt.Sprintf("Expected string, got: %T", req.ProviderData),
"Missing provider configuration",
"The Bitbucket provider was not configured before using this resource.",
)
return
}
r.authHeader = auth
providerData, ok := req.ProviderData.(ProviderData)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Provider Data Type",
fmt.Sprintf("Expected ProviderData, got: %T", req.ProviderData),
)
return
}
if providerData.ServerURL == "" {
resp.Diagnostics.AddError(
"Invalid provider configuration",
"The 'server_url' in provider configuration cannot be empty.",
)
return
}
r.authHeader = providerData.AuthHeader
r.serverURL = providerData.ServerURL
}
func (r *BitbucketTokenResource) getExistingToken(auth, project, repo, name string) (string, error) {
apiURL := fmt.Sprintf("https://stash.ysoft.local/rest/access-tokens/latest/projects/%s/repos/%s?limit=10000", project, repo)
func (r *BitbucketTokenResource) getExistingToken(auth, baseURL, project, repo, name string) (string, error) {
apiURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s?limit=10000", baseURL, project, repo)
client := &http.Client{Timeout: 15 * time.Second}
reqGet, _ := http.NewRequest("GET", apiURL, nil)
@@ -109,14 +134,14 @@ func (r *BitbucketTokenResource) getExistingToken(auth, project, repo, name stri
}
if latestToken == "" {
return "", nil // no active token
return "", nil
}
return latestToken, nil
}
func (r *BitbucketTokenResource) createToken(auth, project, repo, name string) (string, error) {
func (r *BitbucketTokenResource) createToken(auth, baseURL, project, repo, name string) (string, error) {
now := time.Now().UnixMilli()
putURL := fmt.Sprintf("https://stash.ysoft.local/rest/access-tokens/latest/projects/%s/repos/%s", project, repo)
putURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s", baseURL, project, repo)
payload := map[string]interface{}{
"expiryDays": 90,
"name": fmt.Sprintf("%s-%d", name, now),
@@ -141,12 +166,13 @@ func (r *BitbucketTokenResource) createToken(auth, project, repo, name string) (
tok, _ := putJSON["token"].(string)
if tok == "" {
return "", fmt.Errorf("failed to obtain token from API response")
return "", fmt.Errorf("failed to obtain token from API response: %s", string(bodyPut))
}
return tok, nil
}
// Create resource
func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data BitbucketTokenResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
@@ -154,31 +180,30 @@ func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.Create
return
}
// Check if a token already exists
existing, err := r.getExistingToken(
r.authHeader,
r.serverURL,
data.ProjectName.ValueString(),
data.RepositoryName.ValueString(),
data.TokenName.ValueString(),
)
if err != nil {
resp.Diagnostics.AddError("Error checking token", err.Error())
resp.Diagnostics.AddError("Error checking existing token", err.Error())
return
}
if existing != "" {
// token already exists
data.Token = types.StringValue(existing)
} else {
// create new token
token, err := r.createToken(
r.authHeader,
r.serverURL,
data.ProjectName.ValueString(),
data.RepositoryName.ValueString(),
data.TokenName.ValueString(),
)
if err != nil {
resp.Diagnostics.AddError("Error creating token", err.Error())
resp.Diagnostics.AddError("Error creating new token", err.Error())
return
}
data.Token = types.StringValue(token)
@@ -202,6 +227,7 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
existing, err := r.getExistingToken(
r.authHeader,
r.serverURL,
data.ProjectName.ValueString(),
data.RepositoryName.ValueString(),
data.TokenName.ValueString(),
@@ -212,12 +238,10 @@ func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequ
}
if existing == "" {
// Token no longer exists → mark resource gone
resp.State.RemoveResource(ctx)
return
}
// keep same state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -228,9 +252,9 @@ func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.Update
return
}
// Check if a valid token already exists
existing, err := r.getExistingToken(
r.authHeader,
r.serverURL,
data.ProjectName.ValueString(),
data.RepositoryName.ValueString(),
data.TokenName.ValueString(),
@@ -241,12 +265,11 @@ func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.Update
}
if existing != "" {
// Token already valid, no need to recreate
data.Token = types.StringValue(existing)
} else {
// No valid token found → create a new one
token, err := r.createToken(
r.authHeader,
r.serverURL,
data.ProjectName.ValueString(),
data.RepositoryName.ValueString(),
data.TokenName.ValueString(),
@@ -272,20 +295,15 @@ func (r *BitbucketTokenResource) Delete(ctx context.Context, req resource.Delete
project := data.ProjectName.ValueString()
repo := data.RepositoryName.ValueString()
name := data.TokenName.ValueString()
baseURL := r.serverURL
client := &http.Client{Timeout: 15 * time.Second}
// First find the token ID if needed
tokenID, err := r.getExistingToken(
auth,
project,
repo,
name,
)
tokenID, err := r.getExistingToken(auth, baseURL, project, repo, name)
if err != nil {
resp.Diagnostics.AddWarning("Failed to verify token before deletion", err.Error())
} else if tokenID != "" {
apiURL := fmt.Sprintf("https://stash.ysoft.local/rest/access-tokens/latest/projects/%s/repos/%s/%s", project, repo, tokenID)
apiURL := fmt.Sprintf("%s/rest/access-tokens/latest/projects/%s/repos/%s/%s", baseURL, project, repo, tokenID)
reqDel, _ := http.NewRequest("DELETE", apiURL, nil)
reqDel.Header.Add("Authorization", "Basic "+auth)
@@ -304,6 +322,5 @@ func (r *BitbucketTokenResource) Delete(ctx context.Context, req resource.Delete
}
}
// Terraform will remove resource from state regardless
resp.State.RemoveResource(ctx)
}