teamcity/vcsroot.go (696 lines of code) (raw):

package teamcity import ( "context" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "strconv" "terraform-provider-teamcity/client" "terraform-provider-teamcity/models" ) var ( _ resource.Resource = &vcsRootResource{} _ resource.ResourceWithConfigure = &vcsRootResource{} _ resource.ResourceWithImportState = &vcsRootResource{} ) func NewVcsRootResource() resource.Resource { return &vcsRootResource{} } type vcsRootResource struct { client *client.Client } type vcsRootResourceModel struct { Name types.String `tfsdk:"name"` Id types.String `tfsdk:"id"` ProjectId types.String `tfsdk:"project_id"` PollingInterval types.Int64 `tfsdk:"polling_interval"` Git *GitPropertiesModel `tfsdk:"git"` } type GitPropertiesModel struct { Url types.String `tfsdk:"url" teamcity:"url"` PushUrl types.String `tfsdk:"push_url"` Branch types.String `tfsdk:"branch" teamcity:"branch"` BranchSpec types.String `tfsdk:"branch_spec"` TagsAsBranches types.Bool `tfsdk:"tags_as_branches"` UsernameStyle types.String `tfsdk:"username_style"` Submodules types.String `tfsdk:"submodules"` UsernameForTags types.String `tfsdk:"username_for_tags"` AuthMethod types.String `tfsdk:"auth_method"` Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` UploadedKey types.String `tfsdk:"uploaded_key"` PrivateKeyPath types.String `tfsdk:"private_key_path"` Passphrase types.String `tfsdk:"passphrase"` IgnoreKnownHosts types.Bool `tfsdk:"ignore_known_hosts"` ConvertCrlf types.Bool `tfsdk:"convert_crlf"` PathToGit types.String `tfsdk:"path_to_git"` CheckoutPolicy types.String `tfsdk:"checkout_policy"` CleanPolicy types.String `tfsdk:"clean_policy"` CleanFilesPolicy types.String `tfsdk:"clean_files_policy"` TokenId types.String `tfsdk:"token_id"` } func (r *vcsRootResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_vcsroot" } func (r *vcsRootResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "A VCS root in TeamCity defines a connection to a version control system. More info [here](https://www.jetbrains.com/help/teamcity/vcs-root.html)", Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ Required: true, }, "id": schema.StringAttribute{ Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "project_id": schema.StringAttribute{ Required: true, }, "polling_interval": schema.Int64Attribute{ Optional: true, }, "git": schema.SingleNestedAttribute{ Required: true, Attributes: map[string]schema.Attribute{ "url": schema.StringAttribute{ Required: true, }, "push_url": schema.StringAttribute{ Optional: true, }, "branch": schema.StringAttribute{ Required: true, }, "branch_spec": schema.StringAttribute{ Optional: true, }, "tags_as_branches": schema.BoolAttribute{ Optional: true, }, "username_style": schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{ //TODO other syntax? stringvalidator.OneOf([]string{"USERID", "NAME", "EMAIL", "FULL"}...), }, Default: stringdefault.StaticString("USERID"), }, "submodules": schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{ //TODO other syntax? stringvalidator.OneOf([]string{"IGNORE", "CHECKOUT"}...), }, Default: stringdefault.StaticString("CHECKOUT"), }, "username_for_tags": schema.StringAttribute{ Optional: true, }, "auth_method": schema.StringAttribute{ Optional: true, Validators: []validator.String{ stringvalidator.OneOf([]string{ //TODO other syntax? alternate nested types "ANONYMOUS", "PASSWORD", "TEAMCITY_SSH_KEY", "ACCESS_TOKEN", "PRIVATE_KEY_DEFAULT", "PRIVATE_KEY_FILE", }...), }, }, "username": schema.StringAttribute{ Optional: true, }, "password": schema.StringAttribute{ Optional: true, Sensitive: true, }, "uploaded_key": schema.StringAttribute{ Optional: true, }, "private_key_path": schema.StringAttribute{ Optional: true, }, "passphrase": schema.StringAttribute{ Optional: true, Sensitive: true, }, "ignore_known_hosts": schema.BoolAttribute{ Optional: true, Computed: true, Default: booldefault.StaticBool(true), }, "convert_crlf": schema.BoolAttribute{ Optional: true, }, "path_to_git": schema.StringAttribute{ Optional: true, }, "checkout_policy": schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{ stringvalidator.OneOf([]string{"AUTO", "USE_MIRRORS", "NO_MIRRORS", "SHALLOW_CLONE"}...), }, Default: stringdefault.StaticString("AUTO"), }, "clean_policy": schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{ stringvalidator.OneOf([]string{"ON_BRANCH_CHANGE", "ALWAYS", "NEVER"}...), }, Default: stringdefault.StaticString("ON_BRANCH_CHANGE"), }, "clean_files_policy": schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{ stringvalidator.OneOf([]string{"ALL_UNTRACKED", "IGNORED_ONLY", "NON_IGNORED_ONLY"}...), }, Default: stringdefault.StaticString("ALL_UNTRACKED"), }, "token_id": schema.StringAttribute{ Optional: true, }, }, }, }, } } func (r *vcsRootResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { if req.ProviderData == nil { return } r.client = req.ProviderData.(*client.Client) } func (r *vcsRootResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan vcsRootResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } var id *string if plan.Id.IsUnknown() { id = nil } else { val := plan.Id.ValueString() id = &val } root := client.VcsRoot{ Name: plan.Name.ValueString(), Id: id, VcsName: "jetbrains.git", Project: client.ProjectLocator{ Id: plan.ProjectId.ValueString(), }, } props := []models.Property{ {Name: "url", Value: plan.Git.Url.ValueString()}, {Name: "branch", Value: plan.Git.Branch.ValueString()}, } if plan.Git.PushUrl.IsNull() != true { props = append(props, models.Property{Name: "push_url", Value: plan.Git.PushUrl.ValueString()}) } if plan.Git.BranchSpec.IsNull() != true { props = append(props, models.Property{Name: "teamcity:branchSpec", Value: plan.Git.BranchSpec.ValueString()}) } if plan.Git.TagsAsBranches.IsNull() != true { val := strconv.FormatBool(plan.Git.TagsAsBranches.ValueBool()) props = append(props, models.Property{Name: "reportTagRevisions", Value: val}) } if plan.Git.UsernameStyle.IsNull() != true { props = append(props, models.Property{Name: "usernameStyle", Value: plan.Git.UsernameStyle.ValueString()}) } if plan.Git.Submodules.IsNull() != true { props = append(props, models.Property{Name: "submoduleCheckout", Value: plan.Git.Submodules.ValueString()}) } if plan.Git.UsernameForTags.IsNull() != true { props = append(props, models.Property{Name: "userForTags", Value: plan.Git.UsernameForTags.ValueString()}) } if plan.Git.AuthMethod.IsNull() != true { props = append(props, models.Property{Name: "authMethod", Value: plan.Git.AuthMethod.ValueString()}) } if plan.Git.Username.IsNull() != true { props = append(props, models.Property{Name: "username", Value: plan.Git.Username.ValueString()}) } if plan.Git.Password.IsNull() != true { props = append(props, models.Property{Name: "secure:password", Value: plan.Git.Password.ValueString()}) } if plan.Git.UploadedKey.IsNull() != true { props = append(props, models.Property{Name: "teamcitySshKey", Value: plan.Git.UploadedKey.ValueString()}) } if plan.Git.PrivateKeyPath.IsNull() != true { props = append(props, models.Property{Name: "privateKeyPath", Value: plan.Git.PrivateKeyPath.ValueString()}) } if plan.Git.Passphrase.IsNull() != true { props = append(props, models.Property{Name: "secure:passphrase", Value: plan.Git.Passphrase.ValueString()}) } if plan.Git.IgnoreKnownHosts.IsNull() != true { val := strconv.FormatBool(plan.Git.IgnoreKnownHosts.ValueBool()) props = append(props, models.Property{Name: "ignoreKnownHosts", Value: val}) } if plan.Git.ConvertCrlf.IsNull() != true { val := strconv.FormatBool(plan.Git.ConvertCrlf.ValueBool()) props = append(props, models.Property{Name: "serverSideAutoCrlf", Value: val}) } if plan.Git.PathToGit.IsNull() != true { props = append(props, models.Property{Name: "agentGitPath", Value: plan.Git.PathToGit.ValueString()}) } if plan.Git.CheckoutPolicy.IsNull() != true { props = append(props, models.Property{Name: "useAlternates", Value: plan.Git.CheckoutPolicy.ValueString()}) } if plan.Git.CleanPolicy.IsNull() != true { props = append(props, models.Property{Name: "agentCleanPolicy", Value: plan.Git.CleanPolicy.ValueString()}) } if plan.Git.CleanFilesPolicy.IsNull() != true { props = append(props, models.Property{Name: "agentCleanFilesPolicy", Value: plan.Git.CleanFilesPolicy.ValueString()}) } if plan.Git.TokenId.IsNull() != true { props = append(props, models.Property{Name: "tokenId", Value: plan.Git.TokenId.ValueString()}) } root.Properties = models.Properties{ Property: props, } if plan.PollingInterval.IsNull() != true { val := int(plan.PollingInterval.ValueInt64()) root.PollingInterval = &val } actual, err := r.client.NewVcsRoot(root) if err != nil { resp.Diagnostics.AddError( "Error setting VCS root", err.Error(), ) return } newState, err := r.readState(actual) if err != nil { resp.Diagnostics.AddError( "REST returned invalid value: ", err.Error(), ) return } newState.Git.Password = plan.Git.Password newState.Git.Passphrase = plan.Git.Passphrase diags = resp.State.Set(ctx, newState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } func (r *vcsRootResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var oldState vcsRootResourceModel diags := req.State.Get(ctx, &oldState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } actual, err := r.client.GetVcsRoot(oldState.Id.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error Reading VCS root", err.Error(), ) return } if actual == nil { resp.State.RemoveResource(ctx) return } newState, err := r.readState(*actual) if err != nil { resp.Diagnostics.AddError( "REST returned invalid value: ", err.Error(), ) return } if oldState.Git != nil { newState.Git.Password = oldState.Git.Password newState.Git.Passphrase = oldState.Git.Passphrase } diags = resp.State.Set(ctx, newState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } func (r *vcsRootResource) readState(result client.VcsRoot) (vcsRootResourceModel, error) { var state vcsRootResourceModel state.Name = types.StringValue(result.Name) state.Id = types.StringValue(*result.Id) state.ProjectId = types.StringValue(result.Project.Id) if result.PollingInterval != nil { state.PollingInterval = types.Int64Value(int64(*result.PollingInterval)) } props := make(map[string]string) for _, p := range result.Properties.Property { props[p.Name] = p.Value } state.Git = &GitPropertiesModel{ Url: types.StringValue(props["url"]), Branch: types.StringValue(props["branch"]), } if val, ok := props["push_url"]; ok { state.Git.PushUrl = types.StringValue(val) } if val, ok := props["teamcity:branchSpec"]; ok { state.Git.BranchSpec = types.StringValue(val) } if val, ok := props["reportTagRevisions"]; ok { v, err := strconv.ParseBool(val) if err != nil { return vcsRootResourceModel{}, err } state.Git.TagsAsBranches = types.BoolValue(v) } if val, ok := props["usernameStyle"]; ok { state.Git.UsernameStyle = types.StringValue(val) } if val, ok := props["submoduleCheckout"]; ok { state.Git.Submodules = types.StringValue(val) } if val, ok := props["userForTags"]; ok { state.Git.UsernameForTags = types.StringValue(val) } if val, ok := props["authMethod"]; ok { state.Git.AuthMethod = types.StringValue(val) } if val, ok := props["username"]; ok { state.Git.Username = types.StringValue(val) } if val, ok := props["teamcitySshKey"]; ok { state.Git.UploadedKey = types.StringValue(val) } if val, ok := props["privateKeyPath"]; ok { state.Git.PrivateKeyPath = types.StringValue(val) } if val, ok := props["ignoreKnownHosts"]; ok { v, err := strconv.ParseBool(val) if err != nil { return vcsRootResourceModel{}, err } state.Git.IgnoreKnownHosts = types.BoolValue(v) } if val, ok := props["serverSideAutoCrlf"]; ok { v, err := strconv.ParseBool(val) if err != nil { return vcsRootResourceModel{}, err } state.Git.ConvertCrlf = types.BoolValue(v) } if val, ok := props["agentGitPath"]; ok { state.Git.PathToGit = types.StringValue(val) } if val, ok := props["useAlternates"]; ok { state.Git.CheckoutPolicy = types.StringValue(val) } if val, ok := props["agentCleanPolicy"]; ok { state.Git.CleanPolicy = types.StringValue(val) } if val, ok := props["agentCleanFilesPolicy"]; ok { state.Git.CleanFilesPolicy = types.StringValue(val) } if val, ok := props["tokenId"]; ok { state.Git.TokenId = types.StringValue(val) } return state, nil } func (r *vcsRootResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan vcsRootResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } var oldState vcsRootResourceModel diags = req.State.Get(ctx, &oldState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } var newState vcsRootResourceModel newState.Git = &GitPropertiesModel{} resourceId := oldState.Id.ValueString() if result, ok := r.setFieldString(resourceId, "name", oldState.Name, plan.Name, &resp.Diagnostics); ok { newState.Name = result } else { return } if result, ok := r.setFieldString(resourceId, "project", oldState.ProjectId, plan.ProjectId, &resp.Diagnostics); ok { newState.ProjectId = result } else { return } if result, ok := r.setFieldInt(resourceId, "modificationCheckInterval", oldState.PollingInterval, plan.PollingInterval, &resp.Diagnostics); ok { newState.PollingInterval = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/url", oldState.Git.Url, plan.Git.Url, &resp.Diagnostics); ok { newState.Git.Url = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/push_url", oldState.Git.PushUrl, plan.Git.PushUrl, &resp.Diagnostics); ok { newState.Git.PushUrl = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/branch", oldState.Git.Branch, plan.Git.Branch, &resp.Diagnostics); ok { newState.Git.Branch = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/teamcity:branchSpec", oldState.Git.BranchSpec, plan.Git.BranchSpec, &resp.Diagnostics); ok { newState.Git.BranchSpec = result } else { return } if result, ok := r.setFieldBool(resourceId, "properties/reportTagRevisions", oldState.Git.TagsAsBranches, plan.Git.TagsAsBranches, &resp.Diagnostics); ok { newState.Git.TagsAsBranches = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/usernameStyle", oldState.Git.UsernameStyle, plan.Git.UsernameStyle, &resp.Diagnostics); ok { newState.Git.UsernameStyle = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/submoduleCheckout", oldState.Git.Submodules, plan.Git.Submodules, &resp.Diagnostics); ok { newState.Git.Submodules = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/userForTags", oldState.Git.UsernameForTags, plan.Git.UsernameForTags, &resp.Diagnostics); ok { newState.Git.UsernameForTags = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/authMethod", oldState.Git.AuthMethod, plan.Git.AuthMethod, &resp.Diagnostics); ok { newState.Git.AuthMethod = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/username", oldState.Git.Username, plan.Git.Username, &resp.Diagnostics); ok { newState.Git.Username = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/secure:password", oldState.Git.Password, plan.Git.Password, &resp.Diagnostics); ok { newState.Git.Password = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/teamcitySshKey", oldState.Git.UploadedKey, plan.Git.UploadedKey, &resp.Diagnostics); ok { newState.Git.UploadedKey = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/privateKeyPath", oldState.Git.PrivateKeyPath, plan.Git.PrivateKeyPath, &resp.Diagnostics); ok { newState.Git.PrivateKeyPath = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/secure:passphrase", oldState.Git.Passphrase, plan.Git.Passphrase, &resp.Diagnostics); ok { newState.Git.Passphrase = result } else { return } if result, ok := r.setFieldBool(resourceId, "properties/ignoreKnownHosts", oldState.Git.IgnoreKnownHosts, plan.Git.IgnoreKnownHosts, &resp.Diagnostics); ok { newState.Git.IgnoreKnownHosts = result } else { return } if result, ok := r.setFieldBool(resourceId, "properties/serverSideAutoCrlf", oldState.Git.ConvertCrlf, plan.Git.ConvertCrlf, &resp.Diagnostics); ok { newState.Git.ConvertCrlf = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/agentGitPath", oldState.Git.PathToGit, plan.Git.PathToGit, &resp.Diagnostics); ok { newState.Git.PathToGit = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/useAlternates", oldState.Git.CheckoutPolicy, plan.Git.CheckoutPolicy, &resp.Diagnostics); ok { newState.Git.CheckoutPolicy = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/agentCleanPolicy", oldState.Git.CleanPolicy, plan.Git.CleanPolicy, &resp.Diagnostics); ok { newState.Git.CleanPolicy = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/agentCleanFilesPolicy", oldState.Git.CleanFilesPolicy, plan.Git.CleanFilesPolicy, &resp.Diagnostics); ok { newState.Git.CleanFilesPolicy = result } else { return } if result, ok := r.setFieldString(resourceId, "properties/tokenId", oldState.Git.TokenId, plan.Git.TokenId, &resp.Diagnostics); ok { newState.Git.TokenId = result } else { return } if result, ok := r.setFieldString(resourceId, "id", oldState.Id, plan.Id, &resp.Diagnostics); ok { newState.Id = result } else { return } diags = resp.State.Set(ctx, newState) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } func (r *vcsRootResource) setFieldString(id, name string, state, plan types.String, diag *diag.Diagnostics) (types.String, bool) { if plan.Equal(state) { return state, true } var strVal *string if plan.IsNull() { strVal = nil } else { val := plan.ValueString() strVal = &val } result, err := r.client.SetField("vcs-roots", id, name, strVal) if err != nil { diag.AddError( "Error setting VCS root field", err.Error(), ) return types.String{}, false } if result == "" { return types.StringNull(), true } return types.StringValue(result), true } func (r *vcsRootResource) setFieldInt(id, name string, state, plan types.Int64, diag *diag.Diagnostics) (types.Int64, bool) { if plan.Equal(state) { return state, true } var strVal *string if plan.IsNull() { // modificationCheckInterval is the only usage for now, // and it doesn't support DELETE method val := "" strVal = &val } else { val := strconv.FormatInt(plan.ValueInt64(), 10) strVal = &val } result, err := r.client.SetField("vcs-roots", id, name, strVal) if err != nil { diag.AddError( "Error setting VCS root field", err.Error(), ) return types.Int64{}, false } if result == "" { return types.Int64Null(), true } intVal, err := strconv.ParseInt(result, 10, 64) if err != nil { diag.AddError( "Error setting VCS root field", err.Error(), ) return types.Int64{}, false } return types.Int64Value(intVal), true } func (r *vcsRootResource) setFieldBool(id, name string, state, plan types.Bool, diag *diag.Diagnostics) (types.Bool, bool) { if plan.Equal(state) { return state, true } var strVal *string if plan.IsNull() { strVal = nil } else { val := strconv.FormatBool(plan.ValueBool()) strVal = &val } result, err := r.client.SetField("vcs-roots", id, name, strVal) if err != nil { diag.AddError( "Error setting VCS root field", err.Error(), ) return types.Bool{}, false } if result == "" { return types.BoolNull(), true } val, err := strconv.ParseBool(result) if err != nil { diag.AddError( "Error setting VCS root field", err.Error(), ) return types.Bool{}, false } return types.BoolValue(val), true } func (r *vcsRootResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state vcsRootResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } err := r.client.DetachVcsRoot(state.Id.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error detaching VCS root from build configurations", err.Error(), ) return } err = r.client.DeleteVcsRoot(state.Id.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error Deleting VCS root", err.Error(), ) return } } func (r *vcsRootResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) }