diff --git a/docs/index.md b/docs/index.md index 3fdea99..5f3de3d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,6 @@ +--- page_title: "Linear Provider" -description: |- +--- # Linear Provider diff --git a/docs/resources/team.md b/docs/resources/team.md index 6fd9440..5097873 100644 --- a/docs/resources/team.md +++ b/docs/resources/team.md @@ -84,5 +84,5 @@ Optional: Import is supported using the following syntax: ```shell -terraform import linear_team.team SOME +terraform import linear_team.example SOME ``` diff --git a/docs/resources/team_label.md b/docs/resources/team_label.md new file mode 100644 index 0000000..a13bc56 --- /dev/null +++ b/docs/resources/team_label.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "linear_team_label Resource - terraform-provider-linear" +subcategory: "" +description: |- + Linear team label. +--- + +# linear_team_label (Resource) + +Linear team label. + +## Example Usage + +```terraform +resource "linear_team_label" "example" { + name = "Tech Debt" + team_id = linear_team.example.id +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the label. +- `team_id` (String) Identifier of the team. + +### Optional + +- `color` (String) Color of the label. +- `description` (String) Description of the label. + +### Read-Only + +- `id` (String) Identifier of the team. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import linear_team_label.example Bug:SOME +``` diff --git a/examples/resources/linear_team/import.sh b/examples/resources/linear_team/import.sh index 95aafd6..b7886d6 100644 --- a/examples/resources/linear_team/import.sh +++ b/examples/resources/linear_team/import.sh @@ -1 +1 @@ -terraform import linear_team.team SOME +terraform import linear_team.example SOME diff --git a/examples/resources/linear_team_label/import.sh b/examples/resources/linear_team_label/import.sh new file mode 100644 index 0000000..69bd700 --- /dev/null +++ b/examples/resources/linear_team_label/import.sh @@ -0,0 +1 @@ +terraform import linear_team_label.example Bug:SOME diff --git a/examples/resources/linear_team_label/resource.tf b/examples/resources/linear_team_label/resource.tf new file mode 100644 index 0000000..0d30dfb --- /dev/null +++ b/examples/resources/linear_team_label/resource.tf @@ -0,0 +1,4 @@ +resource "linear_team_label" "example" { + name = "Tech Debt" + team_id = linear_team.example.id +} diff --git a/internal/provider/generated.go b/internal/provider/generated.go index c7b9d31..46d747b 100644 --- a/internal/provider/generated.go +++ b/internal/provider/generated.go @@ -8,6 +8,52 @@ import ( "github.com/Khan/genqlient/graphql" ) +type IssueLabelCreateInput struct { + // The identifier. If none is provided, the backend will generate one. + Id string `json:"id,omitempty"` + // The name of the label. + Name string `json:"name"` + // The description of the label. + Description string `json:"description,omitempty"` + // 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"` +} + +// GetId returns IssueLabelCreateInput.Id, and is useful for accessing the field via an interface. +func (v *IssueLabelCreateInput) GetId() string { return v.Id } + +// GetName returns IssueLabelCreateInput.Name, and is useful for accessing the field via an interface. +func (v *IssueLabelCreateInput) GetName() string { return v.Name } + +// GetDescription returns IssueLabelCreateInput.Description, and is useful for accessing the field via an interface. +func (v *IssueLabelCreateInput) GetDescription() string { return v.Description } + +// GetColor returns IssueLabelCreateInput.Color, and is useful for accessing the field via an interface. +func (v *IssueLabelCreateInput) GetColor() string { return v.Color } + +// GetTeamId returns IssueLabelCreateInput.TeamId, and is useful for accessing the field via an interface. +func (v *IssueLabelCreateInput) GetTeamId() string { return v.TeamId } + +type IssueLabelUpdateInput struct { + // The name of the label. + Name string `json:"name,omitempty"` + // The description of the label. + Description string `json:"description,omitempty"` + // The color of the label. + Color string `json:"color,omitempty"` +} + +// GetName returns IssueLabelUpdateInput.Name, and is useful for accessing the field via an interface. +func (v *IssueLabelUpdateInput) GetName() string { return v.Name } + +// GetDescription returns IssueLabelUpdateInput.Description, and is useful for accessing the field via an interface. +func (v *IssueLabelUpdateInput) GetDescription() string { return v.Description } + +// GetColor returns IssueLabelUpdateInput.Color, and is useful for accessing the field via an interface. +func (v *IssueLabelUpdateInput) GetColor() string { return v.Color } + type TeamCreateInput struct { // The identifier. If none is provided, the backend will generate one. Id string `json:"id,omitempty"` @@ -391,6 +437,14 @@ type __createTeamInput struct { // GetInput returns __createTeamInput.Input, and is useful for accessing the field via an interface. func (v *__createTeamInput) GetInput() TeamCreateInput { return v.Input } +// __createTeamLabelInput is used internally by genqlient +type __createTeamLabelInput struct { + Input IssueLabelCreateInput `json:"input"` +} + +// GetInput returns __createTeamLabelInput.Input, and is useful for accessing the field via an interface. +func (v *__createTeamLabelInput) GetInput() IssueLabelCreateInput { return v.Input } + // __deleteTeamInput is used internally by genqlient type __deleteTeamInput struct { Key string `json:"key"` @@ -399,6 +453,26 @@ type __deleteTeamInput struct { // GetKey returns __deleteTeamInput.Key, and is useful for accessing the field via an interface. func (v *__deleteTeamInput) GetKey() string { return v.Key } +// __deleteTeamLabelInput is used internally by genqlient +type __deleteTeamLabelInput struct { + Id string `json:"id"` +} + +// GetId returns __deleteTeamLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__deleteTeamLabelInput) GetId() string { return v.Id } + +// __findTeamLabelInput is used internally by genqlient +type __findTeamLabelInput struct { + Name string `json:"name"` + Key string `json:"key"` +} + +// GetName returns __findTeamLabelInput.Name, and is useful for accessing the field via an interface. +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 } + // __getTeamInput is used internally by genqlient type __getTeamInput struct { Key string `json:"key"` @@ -407,6 +481,14 @@ type __getTeamInput struct { // GetKey returns __getTeamInput.Key, and is useful for accessing the field via an interface. func (v *__getTeamInput) GetKey() string { return v.Key } +// __getTeamLabelInput is used internally by genqlient +type __getTeamLabelInput struct { + Id string `json:"id"` +} + +// GetId returns __getTeamLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__getTeamLabelInput) GetId() string { return v.Id } + // __updateTeamInput is used internally by genqlient type __updateTeamInput struct { Input TeamUpdateInput `json:"input"` @@ -419,6 +501,90 @@ func (v *__updateTeamInput) GetInput() TeamUpdateInput { return v.Input } // GetId returns __updateTeamInput.Id, and is useful for accessing the field via an interface. func (v *__updateTeamInput) GetId() string { return v.Id } +// __updateTeamLabelInput is used internally by genqlient +type __updateTeamLabelInput struct { + Input IssueLabelUpdateInput `json:"input"` + Id string `json:"id"` +} + +// GetInput returns __updateTeamLabelInput.Input, and is useful for accessing the field via an interface. +func (v *__updateTeamLabelInput) GetInput() IssueLabelUpdateInput { return v.Input } + +// GetId returns __updateTeamLabelInput.Id, and is useful for accessing the field via an interface. +func (v *__updateTeamLabelInput) 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. + IssueLabel createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns createTeamLabelIssueLabelCreateIssueLabelPayload.IssueLabel, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayload) GetIssueLabel() createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel { + return v.IssueLabel +} + +// createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel 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"` + // The team that the label is associated with. If null, the label is associated with the global workspace.. + Team createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam `json:"team"` +} + +// GetId returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetId() string { return v.Id } + +// GetName returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetName() string { return v.Name } + +// GetDescription returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetDescription() string { + return v.Description +} + +// GetColor returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetColor() string { + return v.Color +} + +// GetTeam returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel.Team, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabel) GetTeam() createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam { + return v.Team +} + +// createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// An organizational unit that contains issues. +type createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam struct { + // The unique identifier of the entity. + Id string `json:"id"` +} + +// GetId returns createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam.Id, and is useful for accessing the field via an interface. +func (v *createTeamLabelIssueLabelCreateIssueLabelPayloadIssueLabelTeam) GetId() string { return v.Id } + +// createTeamLabelResponse is returned by createTeamLabel on success. +type createTeamLabelResponse struct { + // Creates a new label. + IssueLabelCreate createTeamLabelIssueLabelCreateIssueLabelPayload `json:"issueLabelCreate"` +} + +// GetIssueLabelCreate returns createTeamLabelResponse.IssueLabelCreate, and is useful for accessing the field via an interface. +func (v *createTeamLabelResponse) GetIssueLabelCreate() createTeamLabelIssueLabelCreateIssueLabelPayload { + return v.IssueLabelCreate +} + // createTeamResponse is returned by createTeam on success. type createTeamResponse struct { // Creates a new team. The user who creates the team will automatically be added as a member to the newly created team. @@ -598,6 +764,26 @@ func (v *createTeamTeamCreateTeamPayloadTeam) GetDefaultIssueEstimate() float64 return v.DefaultIssueEstimate } +// deleteTeamLabelIssueLabelDeleteArchivePayload includes the requested fields of the GraphQL type ArchivePayload. +type deleteTeamLabelIssueLabelDeleteArchivePayload struct { + // Whether the operation was successful. + Success bool `json:"success"` +} + +// GetSuccess returns deleteTeamLabelIssueLabelDeleteArchivePayload.Success, and is useful for accessing the field via an interface. +func (v *deleteTeamLabelIssueLabelDeleteArchivePayload) GetSuccess() bool { return v.Success } + +// deleteTeamLabelResponse is returned by deleteTeamLabel on success. +type deleteTeamLabelResponse struct { + // Deletes an issue label. + IssueLabelDelete deleteTeamLabelIssueLabelDeleteArchivePayload `json:"issueLabelDelete"` +} + +// GetIssueLabelDelete returns deleteTeamLabelResponse.IssueLabelDelete, and is useful for accessing the field via an interface. +func (v *deleteTeamLabelResponse) GetIssueLabelDelete() deleteTeamLabelIssueLabelDeleteArchivePayload { + return v.IssueLabelDelete +} + // deleteTeamResponse is returned by deleteTeam on success. type deleteTeamResponse struct { // Deletes a team. @@ -616,6 +802,92 @@ 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 } +// findTeamLabelIssueLabelsIssueLabelConnection includes the requested fields of the GraphQL type IssueLabelConnection. +type findTeamLabelIssueLabelsIssueLabelConnection struct { + Nodes []findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel `json:"nodes"` +} + +// GetNodes returns findTeamLabelIssueLabelsIssueLabelConnection.Nodes, and is useful for accessing the field via an interface. +func (v *findTeamLabelIssueLabelsIssueLabelConnection) GetNodes() []findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel { + return v.Nodes +} + +// findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel struct { + // The unique identifier of the entity. + Id string `json:"id"` +} + +// GetId returns findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *findTeamLabelIssueLabelsIssueLabelConnectionNodesIssueLabel) GetId() string { return v.Id } + +// findTeamLabelResponse is returned by findTeamLabel on success. +type findTeamLabelResponse struct { + // All issue labels. + IssueLabels findTeamLabelIssueLabelsIssueLabelConnection `json:"issueLabels"` +} + +// GetIssueLabels returns findTeamLabelResponse.IssueLabels, and is useful for accessing the field via an interface. +func (v *findTeamLabelResponse) GetIssueLabels() findTeamLabelIssueLabelsIssueLabelConnection { + return v.IssueLabels +} + +// getTeamLabelIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type getTeamLabelIssueLabel 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"` + // The team that the label is associated with. If null, the label is associated with the global workspace.. + Team getTeamLabelIssueLabelTeam `json:"team"` +} + +// GetId returns getTeamLabelIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabel) GetId() string { return v.Id } + +// GetName returns getTeamLabelIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabel) GetName() string { return v.Name } + +// GetDescription returns getTeamLabelIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabel) GetDescription() string { return v.Description } + +// GetColor returns getTeamLabelIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabel) GetColor() string { return v.Color } + +// GetTeam returns getTeamLabelIssueLabel.Team, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabel) GetTeam() getTeamLabelIssueLabelTeam { return v.Team } + +// getTeamLabelIssueLabelTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// An organizational unit that contains issues. +type getTeamLabelIssueLabelTeam struct { + // The unique identifier of the entity. + Id string `json:"id"` +} + +// GetId returns getTeamLabelIssueLabelTeam.Id, and is useful for accessing the field via an interface. +func (v *getTeamLabelIssueLabelTeam) GetId() string { return v.Id } + +// getTeamLabelResponse is returned by getTeamLabel on success. +type getTeamLabelResponse struct { + // One specific label. + IssueLabel getTeamLabelIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns getTeamLabelResponse.IssueLabel, and is useful for accessing the field via an interface. +func (v *getTeamLabelResponse) GetIssueLabel() getTeamLabelIssueLabel { return v.IssueLabel } + // getTeamResponse is returned by getTeam on success. type getTeamResponse struct { // One specific team. @@ -793,6 +1065,78 @@ type getWorkspaceResponse struct { // GetOrganization returns getWorkspaceResponse.Organization, and is useful for accessing the field via an interface. func (v *getWorkspaceResponse) GetOrganization() getWorkspaceOrganization { return v.Organization } +// updateTeamLabelIssueLabelUpdateIssueLabelPayload includes the requested fields of the GraphQL type IssueLabelPayload. +type updateTeamLabelIssueLabelUpdateIssueLabelPayload struct { + // The label that was created or updated. + IssueLabel updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel `json:"issueLabel"` +} + +// GetIssueLabel returns updateTeamLabelIssueLabelUpdateIssueLabelPayload.IssueLabel, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayload) GetIssueLabel() updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel { + return v.IssueLabel +} + +// updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel includes the requested fields of the GraphQL type IssueLabel. +// The GraphQL type's documentation follows. +// +// Labels that can be associated with issues. +type updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel 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"` + // The team that the label is associated with. If null, the label is associated with the global workspace.. + Team updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam `json:"team"` +} + +// GetId returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Id, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetId() string { return v.Id } + +// GetName returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Name, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetName() string { return v.Name } + +// GetDescription returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Description, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetDescription() string { + return v.Description +} + +// GetColor returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Color, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetColor() string { + return v.Color +} + +// GetTeam returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel.Team, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabel) GetTeam() updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam { + return v.Team +} + +// updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam includes the requested fields of the GraphQL type Team. +// The GraphQL type's documentation follows. +// +// An organizational unit that contains issues. +type updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam struct { + // The unique identifier of the entity. + Id string `json:"id"` +} + +// GetId returns updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam.Id, and is useful for accessing the field via an interface. +func (v *updateTeamLabelIssueLabelUpdateIssueLabelPayloadIssueLabelTeam) GetId() string { return v.Id } + +// updateTeamLabelResponse is returned by updateTeamLabel on success. +type updateTeamLabelResponse struct { + // Updates an label. + IssueLabelUpdate updateTeamLabelIssueLabelUpdateIssueLabelPayload `json:"issueLabelUpdate"` +} + +// GetIssueLabelUpdate returns updateTeamLabelResponse.IssueLabelUpdate, and is useful for accessing the field via an interface. +func (v *updateTeamLabelResponse) GetIssueLabelUpdate() updateTeamLabelIssueLabelUpdateIssueLabelPayload { + return v.IssueLabelUpdate +} + // updateTeamResponse is returned by updateTeam on success. type updateTeamResponse struct { // Updates a team. @@ -1031,6 +1375,46 @@ mutation createTeam ($input: TeamCreateInput!) { return &data, err } +func createTeamLabel( + ctx context.Context, + client graphql.Client, + input IssueLabelCreateInput, +) (*createTeamLabelResponse, error) { + req := &graphql.Request{ + OpName: "createTeamLabel", + Query: ` +mutation createTeamLabel ($input: IssueLabelCreateInput!) { + issueLabelCreate(input: $input) { + issueLabel { + id + name + description + color + team { + id + } + } + } +} +`, + Variables: &__createTeamLabelInput{ + Input: input, + }, + } + var err error + + var data createTeamLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func deleteTeam( ctx context.Context, client graphql.Client, @@ -1063,6 +1447,74 @@ mutation deleteTeam ($key: String!) { return &data, err } +func deleteTeamLabel( + ctx context.Context, + client graphql.Client, + id string, +) (*deleteTeamLabelResponse, error) { + req := &graphql.Request{ + OpName: "deleteTeamLabel", + Query: ` +mutation deleteTeamLabel ($id: String!) { + issueLabelDelete(id: $id) { + success + } +} +`, + Variables: &__deleteTeamLabelInput{ + Id: id, + }, + } + var err error + + var data deleteTeamLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + +func findTeamLabel( + ctx context.Context, + client graphql.Client, + name string, + key string, +) (*findTeamLabelResponse, error) { + req := &graphql.Request{ + OpName: "findTeamLabel", + Query: ` +query findTeamLabel ($name: String!, $key: String!) { + issueLabels(filter: {name:{eq:$name},team:{key:{eq:$key}}}) { + nodes { + id + } + } +} +`, + Variables: &__findTeamLabelInput{ + Name: name, + Key: key, + }, + } + var err error + + var data findTeamLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func getTeam( ctx context.Context, client graphql.Client, @@ -1120,6 +1572,44 @@ query getTeam ($key: String!) { return &data, err } +func getTeamLabel( + ctx context.Context, + client graphql.Client, + id string, +) (*getTeamLabelResponse, error) { + req := &graphql.Request{ + OpName: "getTeamLabel", + Query: ` +query getTeamLabel ($id: String!) { + issueLabel(id: $id) { + id + name + description + color + team { + id + } + } +} +`, + Variables: &__getTeamLabelInput{ + Id: id, + }, + } + var err error + + var data getTeamLabelResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + func getWorkspace( ctx context.Context, client graphql.Client, @@ -1210,3 +1700,45 @@ mutation updateTeam ($input: TeamUpdateInput!, $id: String!) { return &data, err } + +func updateTeamLabel( + ctx context.Context, + client graphql.Client, + input IssueLabelUpdateInput, + id string, +) (*updateTeamLabelResponse, error) { + req := &graphql.Request{ + OpName: "updateTeamLabel", + Query: ` +mutation updateTeamLabel ($input: IssueLabelUpdateInput!, $id: String!) { + issueLabelUpdate(input: $input, id: $id) { + issueLabel { + id + name + description + color + team { + id + } + } + } +} +`, + Variables: &__updateTeamLabelInput{ + Input: input, + Id: id, + }, + } + var err error + + var data updateTeamLabelResponse + 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 6669911..fd39820 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -96,7 +96,8 @@ 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": teamResourceType{}, + "linear_team_label": teamLabelResourceType{}, }, nil } diff --git a/internal/provider/resource_team_label.go b/internal/provider/resource_team_label.go new file mode 100644 index 0000000..a55fc10 --- /dev/null +++ b/internal/provider/resource_team_label.go @@ -0,0 +1,235 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "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 = teamLabelResourceType{} +var _ tfsdk.Resource = teamLabelResource{} +var _ tfsdk.ResourceWithImportState = teamLabelResource{} + +type teamLabelResourceType struct{} + +func (t teamLabelResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + MarkdownDescription: "Linear team label.", + Attributes: map[string]tfsdk.Attribute{ + "id": { + MarkdownDescription: "Identifier of the team.", + 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 + }, + }, + "team_id": { + MarkdownDescription: "Identifier of the team.", + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + tfsdk.RequiresReplace(), + }, + Validators: []tfsdk.AttributeValidator{ + // TODO: UUID validation + validators.MinLength(1), + }, + }, + }, + }, nil +} + +func (t teamLabelResourceType) NewResource(ctx context.Context, in tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + provider, diags := convertProviderType(in) + + return teamLabelResource{ + provider: provider, + }, diags +} + +type teamLabelResourceData struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Color types.String `tfsdk:"color"` + TeamId types.String `tfsdk:"team_id"` +} + +type teamLabelResource struct { + provider provider +} + +func (r teamLabelResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var data teamLabelResourceData + + 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, + TeamId: data.TeamId.Value, + } + + response, err := createTeamLabel(context.Background(), r.provider.client, input) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create team label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "created a team 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 teamLabelResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var data teamLabelResourceData + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + response, err := getTeamLabel(context.Background(), r.provider.client, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read team 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} + data.TeamId = types.String{Value: response.IssueLabel.Team.Id} + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r teamLabelResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + var data teamLabelResourceData + + 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 := updateTeamLabel(context.Background(), r.provider.client, input, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update team label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "updated a team 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 teamLabelResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + var data teamLabelResourceData + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + _, err := deleteTeamLabel(context.Background(), r.provider.client, data.Id.Value) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete team label, got error: %s", err)) + return + } + + tflog.Trace(ctx, "deleted a team label") +} + +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] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: label_name:team_key. Got: %q", req.ID), + ) + + return + } + + response, err := findTeamLabel(context.Background(), r.provider.client, parts[0], parts[1]) + + if err != nil || len(response.IssueLabels.Nodes) != 1 { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to import team 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_team_label.graphql b/internal/provider/resource_team_label.graphql new file mode 100644 index 0000000..2962210 --- /dev/null +++ b/internal/provider/resource_team_label.graphql @@ -0,0 +1,73 @@ +query getTeamLabel($id: String!) { + issueLabel(id: $id) { + id + name + description + color + team { + id + } + } +} + +query findTeamLabel($name: String!, $key: String!) { + issueLabels(filter: { + name: { + eq: $name + }, + team: { + key: { + eq: $key + } + } + }) { + nodes { + id + } + } +} + +# @genqlient(for: "IssueLabelCreateInput.id", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.description", omitempty: true) +# @genqlient(for: "IssueLabelCreateInput.color", omitempty: true) +mutation createTeamLabel( + $input: IssueLabelCreateInput! +) { + issueLabelCreate(input: $input) { + issueLabel { + id + name + description + color + team { + id + } + } + } +} + +# @genqlient(for: "IssueLabelUpdateInput.name", omitempty: true) +# @genqlient(for: "IssueLabelUpdateInput.description", omitempty: true) +# @genqlient(for: "IssueLabelUpdateInput.color", omitempty: true) +mutation updateTeamLabel( + $input: IssueLabelUpdateInput!, + $id: String! +) { + issueLabelUpdate(input: $input, id: $id) { + issueLabel { + id + name + description + color + team { + id + } + } + } +} + +mutation deleteTeamLabel($id: String!) { + issueLabelDelete(id: $id) { + success + } +} diff --git a/internal/provider/resource_team_label_test.go b/internal/provider/resource_team_label_test.go new file mode 100644 index 0000000..5efdb4d --- /dev/null +++ b/internal/provider/resource_team_label_test.go @@ -0,0 +1,113 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTeamLabelResourceDefault(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccTeamLabelResourceConfigDefault("Tech Debt"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_team_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "name", "Tech Debt"), + resource.TestCheckResourceAttr("linear_team_label.test", "description", ""), + resource.TestMatchResourceAttr("linear_team_label.test", "color", colorRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "team_id", "4486be5a-706b-47be-81ab-1937d6ecf193"), + ), + }, + // ImportState testing + { + ResourceName: "linear_team_label.test", + ImportState: true, + ImportStateId: "Tech Debt:TEST", + ImportStateVerify: true, + }, + // Update with null values + { + Config: testAccTeamLabelResourceConfigDefault("Tech Debt"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_team_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "name", "Tech Debt"), + resource.TestCheckResourceAttr("linear_team_label.test", "description", ""), + resource.TestMatchResourceAttr("linear_team_label.test", "color", colorRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "team_id", "4486be5a-706b-47be-81ab-1937d6ecf193"), + ), + }, + // Update and Read testing + { + Config: testAccTeamLabelResourceConfigNonDefault("Easy Tech Debt"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_team_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "name", "Easy Tech Debt"), + resource.TestCheckResourceAttr("linear_team_label.test", "description", "lots of it"), + resource.TestCheckResourceAttr("linear_team_label.test", "color", "#00ff00"), + resource.TestCheckResourceAttr("linear_team_label.test", "team_id", "4486be5a-706b-47be-81ab-1937d6ecf193"), + ), + }, + // ImportState testing + { + ResourceName: "linear_team_label.test", + ImportState: true, + ImportStateId: "Easy Tech Debt:TEST", + ImportStateVerify: true, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccTeamLabelResourceNonDefault(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccTeamLabelResourceConfigNonDefault("Needs design"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("linear_team_label.test", "id", uuidRegex()), + resource.TestCheckResourceAttr("linear_team_label.test", "name", "Needs design"), + resource.TestCheckResourceAttr("linear_team_label.test", "description", "lots of it"), + resource.TestCheckResourceAttr("linear_team_label.test", "color", "#00ff00"), + resource.TestCheckResourceAttr("linear_team_label.test", "team_id", "4486be5a-706b-47be-81ab-1937d6ecf193"), + ), + }, + // ImportState testing + { + ResourceName: "linear_team_label.test", + ImportState: true, + ImportStateId: "Needs design:TEST", + ImportStateVerify: true, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccTeamLabelResourceConfigDefault(name string) string { + return fmt.Sprintf(` +resource "linear_team_label" "test" { + name = "%s" + team_id = "4486be5a-706b-47be-81ab-1937d6ecf193" +} +`, name) +} + +func testAccTeamLabelResourceConfigNonDefault(name string) string { + return fmt.Sprintf(` +resource "linear_team_label" "test" { + name = "%s" + description = "lots of it" + color = "#00ff00" + team_id = "4486be5a-706b-47be-81ab-1937d6ecf193" +} +`, name) +} diff --git a/internal/provider/resource_team_test.go b/internal/provider/resource_team_test.go index 1ab2431..2de4df1 100644 --- a/internal/provider/resource_team_test.go +++ b/internal/provider/resource_team_test.go @@ -175,6 +175,10 @@ func colorRegex() *regexp.Regexp { return regexp.MustCompile("^#[0-9a-fA-F]{6}$") } +func uuidRegex() *regexp.Regexp { + return regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") +} + func testAccTeamResourceConfigDefault(key string, name string) string { return fmt.Sprintf(` resource "linear_team" "test" {