Skip to content

Commit

Permalink
Adopt existing channel on create (#113)
Browse files Browse the repository at this point in the history
* Adopt existing channel on create
If a new 'adopt_existing_channel' attribute is set 'true' (default 'false'
for backwards compatibility), an existing channel with the same name
and type will be 'adopted' by the terraform provider, and unarchived
if necessary, to serve as the newly 'created' channel.
This brings parity with the existing provider functionality of archiving
or abandoning channels on terraform resource destroy. Now you can do
a 'terraform apply', 'terraform destroy', and 'terraform apply' again
without errors.

* Make sure api user is in the channel before doing channel
membership operations (needed when adopting existing unarchived
channels)

* Update to terraform-plugin-log v0.4.0
The upstream provider updated the sdk version, which pulled in this
new version. The new version has breaking API changes to logging to
make it more 'structured'.

* Update documentation on conversation api.

* Update go.mod and go.sum for new deps

* Update go.mod to version 0.7.0 of terraform-plugin-log

* added newline

* Made changes to satisfy pedantic linter.

* Address pedantic docs linter whitespace issues.

* Fix test failures
- some channel types (notably private ones) can't be joined.
- ephemeral attributes shouldn't be checked
  • Loading branch information
llamahunter authored Aug 20, 2022
1 parent d3cf37e commit 9d760fc
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 3 deletions.
33 changes: 32 additions & 1 deletion docs/resources/conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,30 @@ Manages a Slack channel

This resource requires the following scopes:

If using `bot` tokens:

- [channels:read](https://api.slack.com/scopes/channels:read)
(public channels)
- [channels:manage](https://api.slack.com/scopes/channels:manage)
(public channels)
- [channels:join](https://api.slack.com/scopes/channels:join)
(adopting existing public channels)
- [groups:read](https://api.slack.com/scopes/groups:read)
(private channels)
- [groups:write](https://api.slack.com/scopes/groups:write)
(private channels)

If using `user` tokens:

- [channels:read](https://api.slack.com/scopes/channels:read) (public channels)
- [channels:manage](https://api.slack.com/scopes/channels:manage) (public channels)
- [channels:write](https://api.slack.com/scopes/channels:manage) (public channels)
- [groups:read](https://api.slack.com/scopes/groups:read) (private channels)
- [groups:write](https://api.slack.com/scopes/groups:write) (private channels)

The Slack API methods used by the resource are:

- [conversations.create](https://api.slack.com/methods/conversations.create)
- [conversations.join](https://api.slack.com/methods/conversations.join)
- [conversations.setTopic](https://api.slack.com/methods/conversations.setTopic)
- [conversations.setPurpose](https://api.slack.com/methods/conversations.setPurpose)
- [conversations.info](https://api.slack.com/methods/conversations.info)
Expand Down Expand Up @@ -53,6 +69,16 @@ resource "slack_conversation" "nonadmin" {
}
```

```hcl
resource "slack_conversation" "adopted" {
name = "my-channel02"
topic = "Adopt existing, don't kick members"
permanent_members = []
adopt_existing_channel = true
action_on_update_permanent_members = "none"
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -72,6 +98,11 @@ name will fail.
whether the members should be kick of the channel when removed from
`permanent_members`. When set to `none` the user are never kicked, this prevent
a side effect on public channels where user that joined the channel are kicked.
- `adopt_existing_channel` (Optional, Default `false`) indicates that an
existing channel with the same name should be adopted by terraform and put under
state management. If the existing channel is archived, it will be unarchived.
(Note: for unarchiving of existing channels to work correctly, you_must_ use
a user token, not a bot token, due to bugs in the Slack API)

## Attribute Reference

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/bflad/tfproviderdocs v0.9.1
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.20.0
github.com/katbyte/terrafmt v0.5.2
github.com/mitchellh/cli v1.1.2 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
Expand Down Expand Up @@ -316,6 +317,7 @@ github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH9
github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
Expand All @@ -327,8 +329,11 @@ github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
github.com/hashicorp/terraform-exec v0.17.2 h1:EU7i3Fh7vDUI9nNRdMATCEfnm9axzTnad8zszYZ73Go=
github.com/hashicorp/terraform-exec v0.17.2/go.mod h1:tuIbsL2l4MlwwIZx9HPM+LOV9vVyEfBYu2GsO1uH3/8=
github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU=
github.com/hashicorp/terraform-json v0.8.0/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE=
github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s=
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
github.com/hashicorp/terraform-plugin-go v0.9.1 h1:vXdHaQ6aqL+OF076nMSBV+JKPdmXlzG5mzVDD04WyPs=
github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M=
github.com/hashicorp/terraform-plugin-go v0.12.0 h1:6wW9mT1dSs0Xq4LR6HXj1heQ5ovr5GxXNJwkErZzpJw=
github.com/hashicorp/terraform-plugin-go v0.12.0/go.mod h1:kwhmaWHNDvT1B3QiSJdAtrB/D4RaKSY/v3r2BuoWK4M=
github.com/hashicorp/terraform-plugin-log v0.6.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4=
Expand Down Expand Up @@ -397,13 +402,15 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
Expand Down Expand Up @@ -560,6 +567,7 @@ github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0=
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
Expand Down Expand Up @@ -807,6 +815,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
Expand Down
83 changes: 82 additions & 1 deletion slack/resource_conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package slack
import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -16,6 +18,10 @@ const (

conversationActionOnUpdatePermanentMembersNone = "none"
conversationActionOnUpdatePermanentMembersKick = "kick"

// 100 is default, slack docs recommend no more than 200, but 1000 is the max.
// See also https://github.com/slack-go/slack/blob/master/users.go#L305
cursorLimit = 200
)

var (
Expand Down Expand Up @@ -112,6 +118,11 @@ func resourceSlackConversation() *schema.Resource {
Default: "kick",
ValidateFunc: validateConversationActionOnUpdatePermanentMembers,
},
"adopt_existing_channel": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}
Expand All @@ -123,6 +134,17 @@ func resourceSlackConversationCreate(ctx context.Context, d *schema.ResourceData
isPrivate := d.Get("is_private").(bool)

channel, err := client.CreateConversationContext(ctx, name, isPrivate)
if err != nil && err.Error() == "name_taken" && d.Get("adopt_existing_channel").(bool) {
channel, err = findExistingChannel(ctx, client, name, isPrivate)
if err == nil && channel.IsArchived {
// ensure unarchived first if adopting existing channel, else other calls below will fail
if err := client.UnArchiveConversationContext(ctx, channel.ID); err != nil {
if err.Error() != "not_archived" {
return diag.Errorf("couldn't unarchive conversation %s: %s", channel.ID, err)
}
}
}
}
if err != nil {
return diag.Errorf("could not create conversation %s: %s", name, err)
}
Expand Down Expand Up @@ -157,6 +179,58 @@ func resourceSlackConversationCreate(ctx context.Context, d *schema.ResourceData
return resourceSlackConversationRead(ctx, d, m)
}

func findExistingChannel(ctx context.Context, client *slack.Client, name string, isPrivate bool) (*slack.Channel, error) {
// find the existing channel. Sadly, there is no non-admin API to search by name,
// so we must search through ALL the channels
tflog.Info(ctx, "Looking for channel %s", map[string]interface{}{"channel": name})
paginationComplete := false
cursor := "" // initial empty cursor to begin at start of list
var types []string // default value with empty list is "public_channel"
if isPrivate {
types = append(types, "private_channel")
}
for !paginationComplete {
channels, nextCursor, err := client.GetConversationsContext(ctx, &slack.GetConversationsParameters{
Cursor: cursor,
Limit: cursorLimit,
Types: types,
})
tflog.Debug(ctx, "new page of channels",
map[string]interface{}{
"numChannels": len(channels),
"nextCursor": nextCursor,
"err": err})
if err != nil {
if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {
tflog.Warn(ctx, "rate limited", map[string]interface{}{"seconds": rateLimitedError.RetryAfter.Seconds()})
select {
case <-ctx.Done():
return nil, fmt.Errorf("canceled during pagination: %s", ctx.Err())
case <-time.After(rateLimitedError.RetryAfter):
tflog.Debug(ctx, "done sleeping after rate limited")
}
// retry current cursor
} else {
return nil, fmt.Errorf("name_taken, but %s trying to find", err)
}
} else {
// see if channel in current batch
for _, c := range channels {
tflog.Trace(ctx, "checking channel", map[string]interface{}{"channel": c.Name})
if c.Name == name {
tflog.Info(ctx, "found channel")
return &c, nil
}
}
// not found so far, move on to next cursor, if pagination incomplete
paginationComplete = nextCursor == ""
cursor = nextCursor
}
}
// looked through entire list, but didn't find matching name
return nil, fmt.Errorf("name_taken, but could not find channel")
}

func updateChannelMembers(ctx context.Context, d *schema.ResourceData, client *slack.Client, channelID string) error {
members := d.Get("permanent_members").(*schema.Set)

Expand All @@ -182,6 +256,13 @@ func updateChannelMembers(ctx context.Context, d *schema.ResourceData, client *s
return fmt.Errorf("could not retrieve conversation users for ID %s: %w", channelID, err)
}

// first, ensure the api user is in the channel, otherwise other member modifications below may fail
if _, _, _, err := client.JoinConversationContext(ctx, channelID); err != nil {
if err.Error() != "already_in_channel" && err.Error() != "method_not_supported_for_channel_type" {
return fmt.Errorf("api user could not join conversation: %w", err)
}
}

action := d.Get("action_on_update_permanent_members").(string)
if action == conversationActionOnUpdatePermanentMembersKick {
for _, currentMember := range channelUsers {
Expand Down Expand Up @@ -265,7 +346,7 @@ func resourceSlackConversationUpdate(ctx context.Context, d *schema.ResourceData
} else {
if err := client.UnArchiveConversationContext(ctx, id); err != nil {
if err.Error() != "not_archived" {
return diag.Errorf("couldn't archive conversation %s: %s", id, err)
return diag.Errorf("couldn't unarchive conversation %s: %s", id, err)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion slack/resource_conversation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func testSlackConversationUpdate(t *testing.T, resourceName string, createChanne
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"permanent_members", "action_on_destroy", "action_on_update_permanent_members"},
ImportStateVerifyIgnore: []string{"permanent_members", "action_on_destroy", "action_on_update_permanent_members", "adopt_existing_channel"},
},
}

Expand Down

0 comments on commit 9d760fc

Please sign in to comment.