From 8958def89ca293379f28b7ac1247d3748bf2b0f5 Mon Sep 17 00:00:00 2001 From: Preclikos Date: Thu, 6 Nov 2025 11:18:51 +0100 Subject: [PATCH] add source code --- README.md | 1 - go.mod | 30 +++++ go.sum | 70 +++++++++++ main.go | 13 ++ provider.go | 63 ++++++++++ resource_token.go | 309 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 485 insertions(+), 1 deletion(-) delete mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 provider.go create mode 100644 resource_token.go diff --git a/README.md b/README.md deleted file mode 100644 index 6d06585..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# terraform-provider-bitbucket \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..35c4f4b --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module terraform-provider-bitbucket-token + +go 1.22 + +require github.com/hashicorp/terraform-plugin-framework v1.9.0 + +require ( + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/terraform-plugin-go v0.23.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.3 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..38c471c --- /dev/null +++ b/go.sum @@ -0,0 +1,70 @@ +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/terraform-plugin-framework v1.9.0 h1:caLcDoxiRucNi2hk8+j3kJwkKfvHznubyFsJMWfZqKU= +github.com/hashicorp/terraform-plugin-framework v1.9.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= +github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= +github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= +github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..83a1d44 --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" +) + +func main() { + providerserver.Serve(context.Background(), NewProvider, providerserver.ServeOpts{ + Address: "local/bitbucket/token", + }) +} diff --git a/provider.go b/provider.go new file mode 100644 index 0000000..cb53427 --- /dev/null +++ b/provider.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func NewProvider() provider.Provider { + return &bitbucketTokenProvider{} +} + +type bitbucketTokenProvider struct{} + +func (p *bitbucketTokenProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "bitbucket" +} + +func (p *bitbucketTokenProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Custom provider for Bitbucket token management.", + 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é + }, + }, + } +} + +func (p *bitbucketTokenProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var config struct { + AuthHeader types.String `tfsdk:"auth_header"` + } + + 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.") + return + } + + resp.DataSourceData = config.AuthHeader.ValueString() + resp.ResourceData = config.AuthHeader.ValueString() +} + +func (p *bitbucketTokenProvider) DataSources(_ context.Context) []func() datasource.DataSource { + return nil +} + +func (p *bitbucketTokenProvider) Resources(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + NewBitbucketTokenResource, + } +} diff --git a/resource_token.go b/resource_token.go new file mode 100644 index 0000000..2efb741 --- /dev/null +++ b/resource_token.go @@ -0,0 +1,309 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type BitbucketTokenResource struct { + authHeader string +} + +func NewBitbucketTokenResource() resource.Resource { + return &BitbucketTokenResource{} +} + +type BitbucketTokenResourceModel struct { + ID types.String `tfsdk:"id"` + TokenName types.String `tfsdk:"token_name"` + ProjectName types.String `tfsdk:"project_name"` + RepositoryName types.String `tfsdk:"repository_name"` + Token types.String `tfsdk:"token"` +} + +func (r *BitbucketTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "bitbucket_token" +} + +func (r *BitbucketTokenResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages Bitbucket access tokens for a repository.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "token_name": schema.StringAttribute{ + Required: true, + }, + "project_name": schema.StringAttribute{ + Required: true, + }, + "repository_name": schema.StringAttribute{ + Required: true, + }, + "token": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +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), + ) + return + } + r.authHeader = auth +} + +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) + client := &http.Client{Timeout: 15 * time.Second} + + reqGet, _ := http.NewRequest("GET", apiURL, nil) + reqGet.Header.Add("Authorization", "Basic "+auth) + respGet, err := client.Do(reqGet) + if err != nil { + return "", err + } + defer respGet.Body.Close() + + body, _ := io.ReadAll(respGet.Body) + var respJSON map[string]interface{} + _ = json.Unmarshal(body, &respJSON) + + values, _ := respJSON["values"].([]interface{}) + now := time.Now().UnixMilli() + var latestExpiry int64 + var latestToken string + + for _, v := range values { + obj, ok := v.(map[string]interface{}) + if !ok { + continue + } + n, _ := obj["name"].(string) + eFloat, _ := obj["expiryDate"].(float64) + e := int64(eFloat) * 1000 + if len(n) >= len(name) && n[:len(name)] == name && e > now && e > latestExpiry { + latestExpiry = e + latestToken = n + } + } + + if latestToken == "" { + return "", nil // no active token + } + return latestToken, nil +} + +func (r *BitbucketTokenResource) createToken(auth, 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) + payload := map[string]interface{}{ + "expiryDays": 90, + "name": fmt.Sprintf("%s-%d", name, now), + "permissions": []string{"REPO_READ"}, + } + + payloadBytes, _ := json.Marshal(payload) + client := &http.Client{Timeout: 15 * time.Second} + reqPut, _ := http.NewRequest("PUT", putURL, bytes.NewReader(payloadBytes)) + reqPut.Header.Add("Authorization", "Basic "+auth) + reqPut.Header.Add("Content-Type", "application/json") + + respPut, err := client.Do(reqPut) + if err != nil { + return "", err + } + defer respPut.Body.Close() + + bodyPut, _ := io.ReadAll(respPut.Body) + var putJSON map[string]interface{} + _ = json.Unmarshal(bodyPut, &putJSON) + + tok, _ := putJSON["token"].(string) + if tok == "" { + return "", fmt.Errorf("failed to obtain token from API response") + } + + return tok, nil +} + +func (r *BitbucketTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data BitbucketTokenResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Check if a token already exists + existing, err := r.getExistingToken( + r.authHeader, + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error checking token", err.Error()) + return + } + + if existing != "" { + // token already exists + data.Token = types.StringValue(existing) + } else { + // create new token + token, err := r.createToken( + r.authHeader, + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error creating token", err.Error()) + return + } + data.Token = types.StringValue(token) + } + + data.ID = types.StringValue(fmt.Sprintf("%s/%s/%s", + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + )) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *BitbucketTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data BitbucketTokenResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + existing, err := r.getExistingToken( + r.authHeader, + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error reading token", err.Error()) + return + } + + 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)...) +} + +func (r *BitbucketTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data BitbucketTokenResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Check if a valid token already exists + existing, err := r.getExistingToken( + r.authHeader, + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error checking existing token", err.Error()) + return + } + + 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, + data.ProjectName.ValueString(), + data.RepositoryName.ValueString(), + data.TokenName.ValueString(), + ) + if err != nil { + resp.Diagnostics.AddError("Error creating new token", err.Error()) + return + } + data.Token = types.StringValue(token) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *BitbucketTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data BitbucketTokenResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + auth := r.authHeader + project := data.ProjectName.ValueString() + repo := data.RepositoryName.ValueString() + name := data.TokenName.ValueString() + + client := &http.Client{Timeout: 15 * time.Second} + + // First find the token ID if needed + tokenID, err := r.getExistingToken( + auth, + 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) + reqDel, _ := http.NewRequest("DELETE", apiURL, nil) + reqDel.Header.Add("Authorization", "Basic "+auth) + + respDel, err := client.Do(reqDel) + if err != nil { + resp.Diagnostics.AddWarning("Error deleting token", err.Error()) + } else { + defer respDel.Body.Close() + if respDel.StatusCode >= 400 { + body, _ := io.ReadAll(respDel.Body) + resp.Diagnostics.AddWarning( + "Bitbucket returned error during delete", + fmt.Sprintf("Status: %s\nBody: %s", respDel.Status, string(body)), + ) + } + } + } + + // Terraform will remove resource from state regardless + resp.State.RemoveResource(ctx) +}