diff --git a/docs/resources/team_label.md b/docs/resources/team_label.md index a13bc56..2cdeb11 100644 --- a/docs/resources/team_label.md +++ b/docs/resources/team_label.md @@ -34,7 +34,7 @@ resource "linear_team_label" "example" { ### Read-Only -- `id` (String) Identifier of the team. +- `id` (String) Identifier of the label. ## Import diff --git a/docs/resources/workspace_label.md b/docs/resources/workspace_label.md new file mode 100644 index 0000000..63fb849 --- /dev/null +++ b/docs/resources/workspace_label.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "linear_workspace_label Resource - terraform-provider-linear" +subcategory: "" +description: |- + Linear workspace label. +--- + +# linear_workspace_label (Resource) + +Linear workspace label. + +## Example Usage + +```terraform +resource "linear_workspace_label" "example" { + name = "Tech Debt" +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the label. + +### Optional + +- `color` (String) Color of the label. +- `description` (String) Description of the label. + +### Read-Only + +- `id` (String) Identifier of the label. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import linear_workspace_label.example Bug +``` diff --git a/examples/resources/linear_workspace_label/import.sh b/examples/resources/linear_workspace_label/import.sh new file mode 100644 index 0000000..323ca41 --- /dev/null +++ b/examples/resources/linear_workspace_label/import.sh @@ -0,0 +1 @@ +terraform import linear_workspace_label.example Bug diff --git a/examples/resources/linear_workspace_label/resource.tf b/examples/resources/linear_workspace_label/resource.tf new file mode 100644 index 0000000..f2aa529 --- /dev/null +++ b/examples/resources/linear_workspace_label/resource.tf @@ -0,0 +1,3 @@ +resource "linear_workspace_label" "example" { + name = "Tech Debt" +} diff --git a/internal/provider/generated.go b/internal/provider/generated.go index 46d747b..44faae2 100644 --- a/internal/provider/generated.go +++ b/internal/provider/generated.go @@ -18,7 +18,7 @@ type IssueLabelCreateInput struct { // The color of the label. Color string `json:"color,omitempty"` // The team associated with the label. If not given, the label will be associated with the entire workspace. - TeamId string `json:"teamId"` + TeamId string `json:"teamId,omitempty"` } // GetId returns IssueLabelCreateInput.Id, and is useful for accessing the field via an interface. @@ -445,6 +445,14 @@ type __createTeamLabelInput struct { // GetInput returns __createTeamLabelInput.Input, and is useful for accessing the field via an interface. func (v *__createTeamLabelInput) GetInput() IssueLabelCreateInput { return v.Input } +// __createWorkspaceLabelInput is used internally by genqlient +type __createWorkspaceLabelInput struct { + Input IssueLabelCreateInput `json:"input"` +} + +// GetInput returns __createWorkspaceLabelInput.Input, and is useful for accessing the field via an interface. +func (v *__createWorkspaceLabelInput) GetInput() IssueLabelCreateInput { return v.Input } + // __deleteTeamInput is used internally by genqlient type __deleteTeamInput struct { Key string `json:"key"` @@ -461,6 +469,14 @@ type __deleteTeamLabelInput struct { // GetId returns __deleteTeamLabelInput.Id, and is useful for accessing the field via an interface. func (v *__deleteTeamLabelInput) GetId() string { return v.Id } +// __deleteWorkspaceLabelInput is used internally by genqlient +type __deleteWorkspaceLabelInput struct { + Id string `json:"id"` +} + +// GetId returns __deleteWorkspaceLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__deleteWorkspaceLabelInput) GetId() string { return v.Id } + // __findTeamLabelInput is used internally by genqlient type __findTeamLabelInput struct { Name string `json:"name"` @@ -473,6 +489,14 @@ func (v *__findTeamLabelInput) GetName() string { return v.Name } // GetKey returns __findTeamLabelInput.Key, and is useful for accessing the field via an interface. func (v *__findTeamLabelInput) GetKey() string { return v.Key } +// __findWorkspaceLabelInput is used internally by genqlient +type __findWorkspaceLabelInput struct { + Name string `json:"name"` +} + +// GetName returns __findWorkspaceLabelInput.Name, and is useful for accessing the field via an interface. +func (v *__findWorkspaceLabelInput) GetName() string { return v.Name } + // __getTeamInput is used internally by genqlient type __getTeamInput struct { Key string `json:"key"` @@ -489,6 +513,14 @@ type __getTeamLabelInput struct { // GetId returns __getTeamLabelInput.Id, and is useful for accessing the field via an interface. func (v *__getTeamLabelInput) GetId() string { return v.Id } +// __getWorkspaceLabelInput is used internally by genqlient +type __getWorkspaceLabelInput struct { + Id string `json:"id"` +} + +// GetId returns __getWorkspaceLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__getWorkspaceLabelInput) GetId() string { return v.Id } + // __updateTeamInput is used internally by genqlient type __updateTeamInput struct { Input TeamUpdateInput `json:"input"` @@ -513,6 +545,18 @@ func (v *__updateTeamLabelInput) GetInput() IssueLabelUpdateInput { return v.Inp // GetId returns __updateTeamLabelInput.Id, and is useful for accessing the field via an interface. func (v *__updateTeamLabelInput) GetId() string { return v.Id } +// __updateWorkspaceLabelInput is used internally by genqlient +type __updateWorkspaceLabelInput struct { + Input IssueLabelUpdateInput `json:"input"` + Id string `json:"id"` +} + +// GetInput returns __updateWorkspaceLabelInput.Input, and is useful for accessing the field via an interface. +func (v *__updateWorkspaceLabelInput) GetInput() IssueLabelUpdateInput { return v.Input } + +// GetId returns __updateWorkspaceLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__updateWorkspaceLabelInput) GetId() string { return v.Id } + // createTeamLabelIssueLabelCreateIssueLabelPayload includes the requested fields of the GraphQL type IssueLabelPayload. type createTeamLabelIssueLabelCreateIssueLabelPayload struct { // The label that was created or updated. @@ -764,6 +808,61 @@ func (v *createTeamTeamCreateTeamPayloadTeam) GetDefaultIssueEstimate() float64 return v.DefaultIssueEstimate } +// createWorkspaceLabelIssueLabelCreateIssueLabelPayload includes the requested fields of the GraphQL type IssueLabelPayload. +type createWorkspaceLabelIssueLabelCreateIssueLabelPayload struct { + // The label that was created or updated. + IssueLabel createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns createWorkspaceLabelIssueLabelCreateIssueLabelPayload.IssueLabel, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelIssueLabelCreateIssueLabelPayload) GetIssueLabel() createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel { + return v.IssueLabel +} + +// createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel struct { + // The unique identifier of the entity. + Id string `json:"id"` + // The label's name. + Name string `json:"name"` + // The label's description. + Description string `json:"description"` + // The label's color as a HEX string. + Color string `json:"color"` +} + +// GetId returns createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetId() string { return v.Id } + +// GetName returns createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetName() string { + return v.Name +} + +// GetDescription returns createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetDescription() string { + return v.Description +} + +// GetColor returns createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetColor() string { + return v.Color +} + +// createWorkspaceLabelResponse is returned by createWorkspaceLabel on success. +type createWorkspaceLabelResponse struct { + // Creates a new label. + IssueLabelCreate createWorkspaceLabelIssueLabelCreateIssueLabelPayload `json:"issueLabelCreate"` +} + +// GetIssueLabelCreate returns createWorkspaceLabelResponse.IssueLabelCreate, and is useful for accessing the field via an interface. +func (v *createWorkspaceLabelResponse) GetIssueLabelCreate() createWorkspaceLabelIssueLabelCreateIssueLabelPayload { + return v.IssueLabelCreate +} + // deleteTeamLabelIssueLabelDeleteArchivePayload includes the requested fields of the GraphQL type ArchivePayload. type deleteTeamLabelIssueLabelDeleteArchivePayload struct { // Whether the operation was successful. @@ -802,6 +901,26 @@ type deleteTeamTeamDeleteArchivePayload struct { // GetSuccess returns deleteTeamTeamDeleteArchivePayload.Success, and is useful for accessing the field via an interface. func (v *deleteTeamTeamDeleteArchivePayload) GetSuccess() bool { return v.Success } +// deleteWorkspaceLabelIssueLabelDeleteArchivePayload includes the requested fields of the GraphQL type ArchivePayload. +type deleteWorkspaceLabelIssueLabelDeleteArchivePayload struct { + // Whether the operation was successful. + Success bool `json:"success"` +} + +// GetSuccess returns deleteWorkspaceLabelIssueLabelDeleteArchivePayload.Success, and is useful for accessing the field via an interface. +func (v *deleteWorkspaceLabelIssueLabelDeleteArchivePayload) GetSuccess() bool { return v.Success } + +// deleteWorkspaceLabelResponse is returned by deleteWorkspaceLabel on success. +type deleteWorkspaceLabelResponse struct { + // Deletes an issue label. + IssueLabelDelete deleteWorkspaceLabelIssueLabelDeleteArchivePayload `json:"issueLabelDelete"` +} + +// GetIssueLabelDelete returns deleteWorkspaceLabelResponse.IssueLabelDelete, and is useful for accessing the field via an interface. +func (v *deleteWorkspaceLabelResponse) GetIssueLabelDelete() deleteWorkspaceLabelIssueLabelDeleteArchivePayload { + return v.IssueLabelDelete +} + // findTeamLabelIssueLabelsIssueLabelConnection includes the requested fields of the GraphQL type IssueLabelConnection. type findTeamLabelIssueLabelsIssueLabelConnection struct { Nodes []findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel `json:"nodes"` @@ -835,6 +954,41 @@ func (v *findTeamLabelResponse) GetIssueLabels() findTeamLabelIssueLabelsIssueLa return v.IssueLabels } +// findWorkspaceLabelIssueLabelsIssueLabelConnection includes the requested fields of the GraphQL type IssueLabelConnection. +type findWorkspaceLabelIssueLabelsIssueLabelConnection struct { + Nodes []findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel `json:"nodes"` +} + +// GetNodes returns findWorkspaceLabelIssueLabelsIssueLabelConnection.Nodes, and is useful for accessing the field via an interface. +func (v *findWorkspaceLabelIssueLabelsIssueLabelConnection) GetNodes() []findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel { + return v.Nodes +} + +// findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel struct { + // The unique identifier of the entity. + Id string `json:"id"` +} + +// GetId returns findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *findWorkspaceLabelIssueLabelsIssueLabelConnectionNodesIssueLabel) GetId() string { + return v.Id +} + +// findWorkspaceLabelResponse is returned by findWorkspaceLabel on success. +type findWorkspaceLabelResponse struct { + // All issue labels. + IssueLabels findWorkspaceLabelIssueLabelsIssueLabelConnection `json:"issueLabels"` +} + +// GetIssueLabels returns findWorkspaceLabelResponse.IssueLabels, and is useful for accessing the field via an interface. +func (v *findWorkspaceLabelResponse) GetIssueLabels() findWorkspaceLabelIssueLabelsIssueLabelConnection { + return v.IssueLabels +} + // getTeamLabelIssueLabel includes the requested fields of the GraphQL type IssueLabel. // The GraphQL type's documentation follows. // @@ -1034,6 +1188,42 @@ func (v *getTeamTeam) GetIssueEstimationExtended() bool { return v.IssueEstimati // GetDefaultIssueEstimate returns getTeamTeam.DefaultIssueEstimate, and is useful for accessing the field via an interface. func (v *getTeamTeam) GetDefaultIssueEstimate() float64 { return v.DefaultIssueEstimate } +// getWorkspaceLabelIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type getWorkspaceLabelIssueLabel struct { + // The unique identifier of the entity. + Id string `json:"id"` + // The label's name. + Name string `json:"name"` + // The label's description. + Description string `json:"description"` + // The label's color as a HEX string. + Color string `json:"color"` +} + +// GetId returns getWorkspaceLabelIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *getWorkspaceLabelIssueLabel) GetId() string { return v.Id } + +// GetName returns getWorkspaceLabelIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *getWorkspaceLabelIssueLabel) GetName() string { return v.Name } + +// GetDescription returns getWorkspaceLabelIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *getWorkspaceLabelIssueLabel) GetDescription() string { return v.Description } + +// GetColor returns getWorkspaceLabelIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *getWorkspaceLabelIssueLabel) GetColor() string { return v.Color } + +// getWorkspaceLabelResponse is returned by getWorkspaceLabel on success. +type getWorkspaceLabelResponse struct { + // One specific label. + IssueLabel getWorkspaceLabelIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns getWorkspaceLabelResponse.IssueLabel, and is useful for accessing the field via an interface. +func (v *getWorkspaceLabelResponse) GetIssueLabel() getWorkspaceLabelIssueLabel { return v.IssueLabel } + // getWorkspaceOrganization includes the requested fields of the GraphQL type Organization. // The GraphQL type's documentation follows. // @@ -1316,6 +1506,61 @@ func (v *updateTeamTeamUpdateTeamPayloadTeam) GetDefaultIssueEstimate() float64 return v.DefaultIssueEstimate } +// updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload includes the requested fields of the GraphQL type IssueLabelPayload. +type updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload struct { + // The label that was created or updated. + IssueLabel updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload.IssueLabel, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload) GetIssueLabel() updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel { + return v.IssueLabel +} + +// updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel struct { + // The unique identifier of the entity. + Id string `json:"id"` + // The label's name. + Name string `json:"name"` + // The label's description. + Description string `json:"description"` + // The label's color as a HEX string. + Color string `json:"color"` +} + +// GetId returns updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetId() string { return v.Id } + +// GetName returns updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetName() string { + return v.Name +} + +// GetDescription returns updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetDescription() string { + return v.Description +} + +// GetColor returns updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetColor() string { + return v.Color +} + +// updateWorkspaceLabelResponse is returned by updateWorkspaceLabel on success. +type updateWorkspaceLabelResponse struct { + // Updates an label. + IssueLabelUpdate updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload `json:"issueLabelUpdate"` +} + +// GetIssueLabelUpdate returns updateWorkspaceLabelResponse.IssueLabelUpdate, and is useful for accessing the field via an interface. +func (v *updateWorkspaceLabelResponse) GetIssueLabelUpdate() updateWorkspaceLabelIssueLabelUpdateIssueLabelPayload { + return v.IssueLabelUpdate +} + func createTeam( ctx context.Context, client graphql.Client, @@ -1415,6 +1660,43 @@ mutation createTeamLabel ($input: IssueLabelCreateInput!) { return &data, err } +func createWorkspaceLabel( + ctx context.Context, + client graphql.Client, + input IssueLabelCreateInput, +) (*createWorkspaceLabelResponse, error) { + req := &graphql.Request{ + OpName: "createWorkspaceLabel", + Query: ` +mutation createWorkspaceLabel ($input: IssueLabelCreateInput!) { + issueLabelCreate(input: $input) { + issueLabel { + id + name + description + color + } + } +} +`, + Variables: &__createWorkspaceLabelInput{ + Input: input, + }, + } + var err error + + var data createWorkspaceLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func deleteTeam( ctx context.Context, client graphql.Client, @@ -1479,6 +1761,38 @@ mutation deleteTeamLabel ($id: String!) { return &data, err } +func deleteWorkspaceLabel( + ctx context.Context, + client graphql.Client, + id string, +) (*deleteWorkspaceLabelResponse, error) { + req := &graphql.Request{ + OpName: "deleteWorkspaceLabel", + Query: ` +mutation deleteWorkspaceLabel ($id: String!) { + issueLabelDelete(id: $id) { + success + } +} +`, + Variables: &__deleteWorkspaceLabelInput{ + Id: id, + }, + } + var err error + + var data deleteWorkspaceLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func findTeamLabel( ctx context.Context, client graphql.Client, @@ -1515,6 +1829,40 @@ query findTeamLabel ($name: String!, $key: String!) { return &data, err } +func findWorkspaceLabel( + ctx context.Context, + client graphql.Client, + name string, +) (*findWorkspaceLabelResponse, error) { + req := &graphql.Request{ + OpName: "findWorkspaceLabel", + Query: ` +query findWorkspaceLabel ($name: String!) { + issueLabels(filter: {name:{eq:$name}}) { + nodes { + id + } + } +} +`, + Variables: &__findWorkspaceLabelInput{ + Name: name, + }, + } + var err error + + var data findWorkspaceLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func getTeam( ctx context.Context, client graphql.Client, @@ -1640,6 +1988,41 @@ query getWorkspace { return &data, err } +func getWorkspaceLabel( + ctx context.Context, + client graphql.Client, + id string, +) (*getWorkspaceLabelResponse, error) { + req := &graphql.Request{ + OpName: "getWorkspaceLabel", + Query: ` +query getWorkspaceLabel ($id: String!) { + issueLabel(id: $id) { + id + name + description + color + } +} +`, + Variables: &__getWorkspaceLabelInput{ + Id: id, + }, + } + var err error + + var data getWorkspaceLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func updateTeam( ctx context.Context, client graphql.Client, @@ -1742,3 +2125,42 @@ mutation updateTeamLabel ($input: IssueLabelUpdateInput!, $id: String!) { return &data, err } + +func updateWorkspaceLabel( + ctx context.Context, + client graphql.Client, + input IssueLabelUpdateInput, + id string, +) (*updateWorkspaceLabelResponse, error) { + req := &graphql.Request{ + OpName: "updateWorkspaceLabel", + Query: ` +mutation updateWorkspaceLabel ($input: IssueLabelUpdateInput!, $id: String!) { + issueLabelUpdate(input: $input, id: $id) { + issueLabel { + id + name + description + color + } + } +} +`, + Variables: &__updateWorkspaceLabelInput{ + Input: input, + Id: id, + }, + } + var err error + + var data updateWorkspaceLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fd39820..f5383b8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -96,8 +96,9 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq func (p *provider) GetResources(ctx context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ - "linear_team": teamResourceType{}, - "linear_team_label": teamLabelResourceType{}, + "linear_team": teamResourceType{}, + "linear_team_label": teamLabelResourceType{}, + "linear_workspace_label": workspaceLabelResourceType{}, }, nil } diff --git a/internal/provider/resource_team.go b/internal/provider/resource_team.go index ecf2d20..6930a4b 100644 --- a/internal/provider/resource_team.go +++ b/internal/provider/resource_team.go @@ -89,6 +89,7 @@ func (t teamResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia Required: true, Validators: []tfsdk.AttributeValidator{ validators.MinLength(1), + validators.NoWhitespace(), }, }, "name": { diff --git a/internal/provider/resource_team_label.go b/internal/provider/resource_team_label.go index a55fc10..063ef01 100644 --- a/internal/provider/resource_team_label.go +++ b/internal/provider/resource_team_label.go @@ -25,7 +25,7 @@ func (t teamLabelResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, dia MarkdownDescription: "Linear team label.", Attributes: map[string]tfsdk.Attribute{ "id": { - MarkdownDescription: "Identifier of the team.", + MarkdownDescription: "Identifier of the label.", Type: types.StringType, Computed: true, PlanModifiers: tfsdk.AttributePlanModifiers{ @@ -211,8 +211,6 @@ func (r teamLabelResource) Delete(ctx context.Context, req tfsdk.DeleteResourceR } func (r teamLabelResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - tfsdk.ResourceImportStatePassthroughID(ctx, tftypes.NewAttributePath().WithAttributeName("id"), req, resp) - parts := strings.Split(req.ID, ":") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { diff --git a/internal/provider/resource_team_label.graphql b/internal/provider/resource_team_label.graphql index 2962210..52b6ed2 100644 --- a/internal/provider/resource_team_label.graphql +++ b/internal/provider/resource_team_label.graphql @@ -30,6 +30,7 @@ query findTeamLabel($name: String!, $key: String!) { # @genqlient(for: "IssueLabelCreateInput.id", omitempty: true) # @genqlient(for: "IssueLabelCreateInput.description", omitempty: true) # @genqlient(for: "IssueLabelCreateInput.color", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.teamId", omitempty: true) mutation createTeamLabel( $input: IssueLabelCreateInput! ) { diff --git a/internal/provider/resource_workspace_label.go b/internal/provider/resource_workspace_label.go new file mode 100644 index 0000000..acde399 --- /dev/null +++ b/internal/provider/resource_workspace_label.go @@ -0,0 +1,206 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/frankgreco/terraform-helpers/validators" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ tfsdk.ResourceType = workspaceLabelResourceType{} +var _ tfsdk.Resource = workspaceLabelResource{} +var _ tfsdk.ResourceWithImportState = workspaceLabelResource{} + +type workspaceLabelResourceType struct{} + +func (t workspaceLabelResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + MarkdownDescription: "Linear workspace label.", + Attributes: map[string]tfsdk.Attribute{ + "id": { + MarkdownDescription: "Identifier of the label.", + Type: types.StringType, + Computed: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + tfsdk.UseStateForUnknown(), + }, + }, + "name": { + MarkdownDescription: "Name of the label.", + Type: types.StringType, + Required: true, + Validators: []tfsdk.AttributeValidator{ + validators.MinLength(1), + }, + }, + "description": { + MarkdownDescription: "Description of the label.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + tfsdk.UseStateForUnknown(), + }, + }, + "color": { + MarkdownDescription: "Color of the label.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + tfsdk.UseStateForUnknown(), + }, + Validators: []tfsdk.AttributeValidator{ + // TODO: Color value validation + }, + }, + }, + }, nil +} + +func (t workspaceLabelResourceType) NewResource(ctx context.Context, in tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + provider, diags := convertProviderType(in) + + return workspaceLabelResource{ + provider: provider, + }, diags +} + +type workspaceLabelResourceData struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Color types.String `tfsdk:"color"` +} + +type workspaceLabelResource struct { + provider provider +} + +func (r workspaceLabelResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var data workspaceLabelResourceData + + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + input := IssueLabelCreateInput{ + Name: data.Name.Value, + Description: data.Description.Value, + Color: data.Color.Value, + } + + response, err := createWorkspaceLabel(context.Background(), r.provider.client, input) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create workspace label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "created a workspace label") + + data.Id = types.String{Value: response.IssueLabelCreate.IssueLabel.Id} + data.Description = types.String{Value: response.IssueLabelCreate.IssueLabel.Description} + data.Color = types.String{Value: response.IssueLabelCreate.IssueLabel.Color} + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r workspaceLabelResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var data workspaceLabelResourceData + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + response, err := getWorkspaceLabel(context.Background(), r.provider.client, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read workspace label, got error: %s", err)) + return + } + + data.Name = types.String{Value: response.IssueLabel.Name} + data.Description = types.String{Value: response.IssueLabel.Description} + data.Color = types.String{Value: response.IssueLabel.Color} + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r workspaceLabelResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + var data workspaceLabelResourceData + + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + input := IssueLabelUpdateInput{ + Name: data.Name.Value, + Description: data.Description.Value, + Color: data.Color.Value, + } + + response, err := updateWorkspaceLabel(context.Background(), r.provider.client, input, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update workspace label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "updated a workspace label") + + data.Name = types.String{Value: response.IssueLabelUpdate.IssueLabel.Name} + data.Description = types.String{Value: response.IssueLabelUpdate.IssueLabel.Description} + data.Color = types.String{Value: response.IssueLabelUpdate.IssueLabel.Color} + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r workspaceLabelResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + var data workspaceLabelResourceData + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := deleteWorkspaceLabel(context.Background(), r.provider.client, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete workspace label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "deleted a workspace label") +} + +func (r workspaceLabelResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + response, err := findWorkspaceLabel(context.Background(), r.provider.client, req.ID) + + if err != nil || len(response.IssueLabels.Nodes) != 1 { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to import workspace label, got error: %s", err)) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, tftypes.NewAttributePath().WithAttributeName("id"), response.IssueLabels.Nodes[0].Id)...) +} diff --git a/internal/provider/resource_workspace_label.graphql b/internal/provider/resource_workspace_label.graphql new file mode 100644 index 0000000..96d6da6 --- /dev/null +++ b/internal/provider/resource_workspace_label.graphql @@ -0,0 +1,66 @@ +query getWorkspaceLabel($id: String!) { + issueLabel(id: $id) { + id + name + description + color + } +} + +query findWorkspaceLabel($name: String!) { + issueLabels(filter: { + name: { + eq: $name + }, + #4 + # team: { + # key: { + # eq: $key + # } + # } + }) { + nodes { + id + } + } +} + +# @genqlient(for: "IssueLabelCreateInput.id", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.description", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.color", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.teamId", omitempty: true) +mutation createWorkspaceLabel( + $input: IssueLabelCreateInput! +) { + issueLabelCreate(input: $input) { + issueLabel { + id + name + description + color + } + } +} + +# @genqlient(for: "IssueLabelUpdateInput.name", omitempty: true) +# @genqlient(for: "IssueLabelUpdateInput.description", omitempty: true) +# @genqlient(for: "IssueLabelUpdateInput.color", omitempty: true) +mutation updateWorkspaceLabel( + $input: IssueLabelUpdateInput!, + $id: String! +) { + issueLabelUpdate(input: $input, id: $id) { + issueLabel { + id + name + description + color + } + } +} + +mutation deleteWorkspaceLabel($id: String!) { + issueLabelDelete(id: $id) { + success + } +} diff --git a/internal/provider/resource_workspace_label_test.go b/internal/provider/resource_workspace_label_test.go new file mode 100644 index 0000000..a0aca5e --- /dev/null +++ b/internal/provider/resource_workspace_label_test.go @@ -0,0 +1,110 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccWorkspaceLabelResourceDefault(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccWorkspaceLabelResourceConfigDefault("UX"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_workspace_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_workspace_label.test", "name", "UX"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "description", ""), + resource.TestMatchResourceAttr("linear_workspace_label.test", "color", colorRegex()), + ), + }, + // ImportState testing + // #4 + // { + // ResourceName: "linear_workspace_label.test", + // ImportState: true, + // ImportStateId: "UX", + // ImportStateVerify: true, + // }, + // Update with null values + { + Config: testAccWorkspaceLabelResourceConfigDefault("UX"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_workspace_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_workspace_label.test", "name", "UX"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "description", ""), + resource.TestMatchResourceAttr("linear_workspace_label.test", "color", colorRegex()), + ), + }, + // Update and Read testing + { + Config: testAccWorkspaceLabelResourceConfigNonDefault("Easy UX"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_workspace_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_workspace_label.test", "name", "Easy UX"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "description", "lots of it"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "color", "#00ff00"), + ), + }, + // ImportState testing + // #4 + // { + // ResourceName: "linear_workspace_label.test", + // ImportState: true, + // ImportStateId: "Easy UX", + // ImportStateVerify: true, + // }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccWorkspaceLabelResourceNonDefault(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccWorkspaceLabelResourceConfigNonDefault("Needs product"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_workspace_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_workspace_label.test", "name", "Needs product"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "description", "lots of it"), + resource.TestCheckResourceAttr("linear_workspace_label.test", "color", "#00ff00"), + ), + }, + // ImportState testing + // #4 + // { + // ResourceName: "linear_workspace_label.test", + // ImportState: true, + // ImportStateId: "Needs product", + // ImportStateVerify: true, + // }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccWorkspaceLabelResourceConfigDefault(name string) string { + return fmt.Sprintf(` +resource "linear_workspace_label" "test" { + name = "%s" +} +`, name) +} + +func testAccWorkspaceLabelResourceConfigNonDefault(name string) string { + return fmt.Sprintf(` +resource "linear_workspace_label" "test" { + name = "%s" + description = "lots of it" + color = "#00ff00" +} +`, name) +}