Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt existing channel on create #113

Merged
merged 10 commits into from
Aug 20, 2022

Conversation

llamahunter
Copy link
Contributor

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.

Fixes #112

go.mod Outdated
@@ -14,6 +14,7 @@ require (
github.com/hashicorp/go-getter v1.5.5 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.10.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.2.0 // indirect
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using tflog for some diagnostic output on the new code I added. Probably should be more logging throughout?

@@ -107,6 +114,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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we get the 'name_taken' error, and the new 'adopt_exsting_channel' flag is true, then find the existing channel and use it instead of a newly created channel.

Comment on lines +120 to +141
// ensure unarchived first if adopting existing channel, else other calls below will fail
if err := client.UnArchiveConversationContext(ctx, channel.ID); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the channel is currently archived, even if it is configured to eventually be archived, we need to unarchive it as many of the calls below will fail if called on an archived channel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE! this api does not work with bot tokens (despite documentation to the contrary). The provider must be configured with a user token.

Comment on lines +122 to +143
if err.Error() != "not_archived" {
return diag.Errorf("couldn't unarchive conversation %s: %s", channel.ID, err)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we really should never get this error, as we check that it's archived before unarchived, but I guess there could be race conditions with a user unarchiving it manually in between the two calls.

Comment on lines +163 to +184
// find the existing channel. Sadly, there is no non-admin API to search by name,
// so we must search through ALL the channels
Copy link
Contributor Author

@llamahunter llamahunter May 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extreme sadness with the existing slack api. There is an admin api for searching for a specific channel, but that would greatly limit the deployment ability of this provider, as it would need to be configured with an administrator user token.

Comment on lines 180 to 181
if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {
tflog.Warn(ctx, "rate limited for %f seconds", rateLimitedError.RetryAfter.Seconds())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some slack workspaces have thousands of channels, so even going 200 at a time will possibly hit rate limiting. So we need to handle waiting and retrying.

Comment on lines +182 to +211
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")
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similarly implemented to the internals of the paginated GetUsersContext in the slack-go library that this provider is built upon.
https://github.com/slack-go/slack/blob/34c7fd379e3526317abcc057ef7baa686cb0a42c/users.go#L379

Comment on lines 194 to 220
for _, c := range channels {
tflog.Trace(ctx, "current channel %s", c.Name)
if c.Name == name {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check every single channel, but bail as soon as we find a match

@@ -246,7 +312,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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an unarchive, so the error was inaccurate.

@@ -246,7 +312,7 @@ func resourceSlackConversationUpdate(ctx context.Context, d *schema.ResourceData
} else {
if err := client.UnArchiveConversationContext(ctx, id); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note as above that this call does NOT work with bot tokens, only user tokens. :-(

@pablovarela
Copy link
Owner

pablovarela commented Jun 13, 2022

Thank you very much for your contribution! And apologies for the late response. I've been quite busy and neglected this project.

The change looks fine. In my view There are two things lacking: tests and documentation.

I would ask you to at least document the new parameter adopt_existing_channel in the conversation resource documentation (docs/resources/conversation.md), and make it very clear that user tokens are required to make it work properly.

Could you also update the PR with the latest changes in master? There are conflicts that can not be automerged. I will allow linters and tests to run for this PR.

Once that is done I will happily merge it and release a new version.

@llamahunter
Copy link
Contributor Author

ok, thx! Will look into docs and whatever tests I can figure out. I actually wasn't sure how to test this, since it seems to want credentials for a slack account and I can't have it run its tests on our corporate account.

@pablovarela
Copy link
Owner

Oh I see. Yes, you need a slack token to run the tests, set using SLACK_TOKEN env variable. When you push the changes they should run here, if not I can run them locally for you. Not ideal but better than nothing.

@llamahunter llamahunter force-pushed the unarchive-channel-on-create branch from 05f9b93 to 1ffdcc6 Compare July 11, 2022 19:36
- [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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot token separates 'join' permission from 'manage'


If using `user` tokens:
- [channels:read](https://api.slack.com/scopes/channels:read) (public channels)
- [channels:write](https://api.slack.com/scopes/channels:manage) (public channels)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user token uses 'write' rather than 'manage', and 'write' includes join permission already.

- [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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to be able to join existing unarchived channels

Comment on lines 65 to 80
```hcl
resource "slack_conversation" "adopted" {
name = "my-channel02"
topic = "If already existing, channel will be adopted, and existing users not kicked"
permanent_members = []
adopt_existing_channel = true
remove_non_members = false
}
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of the two new parameters in use

Comment on lines 87 to 96
`archive | none`. Note that when set to `none` the conversation will be left
as it is and as a result any subsequent runs of terraform apply with the same
name will fail.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I think, regardless of whether archive or none is specified, subsequent applies will fail unless adopt_existing_channel is set to true.

- `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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the note about the need for 'user' tokens

Comment on lines 95 to 98
- `remove_non_members` (Optional, Default `true`) indicates that members of the
channel that aren't listed in the `permament_members` array will be kicked. (Note:
your Slack configuration may restrict/prohibit kicking of members from channels
except when using administrator credentials)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is especially needed for adopting existing channels, as permission to remove non members may be denied by slack configuration.

@llamahunter
Copy link
Contributor Author

@pablovarela I added docs, and also added another needed parameter to make this work on slack servers that have restricted 'kick' permissions.
Still not clear how to test this w/o a specially designated slack server.

@pablovarela
Copy link
Owner

pablovarela commented Jul 26, 2022

The remove_non_members option has been superseded by #126. and it is now part of v1.1.21 See documentation here.

If you could remove remove_non_members and update the docs I will test it locally and merge this PR asap. I will keep an eye in it this week.

@llamahunter
Copy link
Contributor Author

Ok, I saw that other PR, but looked like it wasn't going anywhere, and this was critically needed to make the adopting of existing channels work correctly. Will re-base on master and resubmit the PR sometime later today.

@llamahunter llamahunter force-pushed the unarchive-channel-on-create branch from 4299839 to 5290d3f Compare July 27, 2022 00:12
@llamahunter
Copy link
Contributor Author

@pablovarela rebased on master, removing remove_non_members flag.

@llamahunter
Copy link
Contributor Author

@pablovarela any chance this could get merged soon?

@llamahunter llamahunter force-pushed the unarchive-channel-on-create branch from 5290d3f to f137252 Compare July 29, 2022 18:50
@llamahunter
Copy link
Contributor Author

@pablovarela I see there was another PR that jumped ahead of this one, causing merge conflicts again. Rebased again. Please review and merge ASAP.

@llamahunter
Copy link
Contributor Author

@pablovarela ping

@pablovarela
Copy link
Owner

on it

Copy link
Owner

@pablovarela pablovarela left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some linting issues (a magic number and some markdown ones), and if I ignore linting the tests are not passing. You can run the tests locally using make.

I will provide you with a token that I will change later, so you can test. Please set the environment variable SLACK_TOKEN=xoxp-1437037581287-1448689260021-1448696433877-1e561acc1efe312956ea7b00d629f805 and run the tests

for !paginationComplete {
channels, nextCursor, err := client.GetConversationsContext(ctx, &slack.GetConversationsParameters{
Cursor: cursor,
Limit: 200, // 100 is default, docs recommend no more than 200, but 1000 is the max
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this magic number is from the documentation of the slack api. See, for instance, a similar usage in their golang library.
https://github.com/slack-go/slack/blob/master/users.go#L305

@llamahunter
Copy link
Contributor Author

made more changes to satisfy linter, at cost of making documentation harder to read in edit mode.

@llamahunter
Copy link
Contributor Author

@pablovarela

@llamahunter llamahunter requested a review from pablovarela August 9, 2022 16:18
Copy link
Owner

@pablovarela pablovarela left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter is still complaining. If you run make docs-lint-fix it will fix the remaining linter issues.

I run that locally, and the linter passes. But the tests still fails, for example

=== CONT  TestAccSlackConversationTest/add_permanent_members
    resource_conversation_test.go:162: Step 1/3 error: Error running apply: exit status 1
        
        Error: api user could not join conversation: method_not_supported_for_channel_type
        
          with slack_conversation.test,
          on terraform_plugin_test.tf line 2, in resource "slack_conversation" "test":
           2: resource slack_conversation test {

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.
membership operations (needed when adopting existing unarchived
channels)
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'.
@llamahunter llamahunter force-pushed the unarchive-channel-on-create branch from a8fcfa6 to e6cf23a Compare August 20, 2022 00:28
- some channel types (notably private ones) can't be joined.
- ephemeral attributes shouldn't be checked
@pablovarela pablovarela merged commit 9d760fc into pablovarela:master Aug 20, 2022
@pablovarela
Copy link
Owner

This is not part of 1.1.24 release.

Thank you so much for your contribution and your patience.

@llamahunter
Copy link
Contributor Author

This is not part of 1.1.24 release.

Thank you so much for your contribution and your patience.

Thx!

This is not part of 1.1.24? It looks like it's in the release notes, tho.

@llamahunter llamahunter deleted the unarchive-channel-on-create branch August 29, 2022 17:57
@pablovarela
Copy link
Owner

pablovarela commented Aug 30, 2022

it is part of 1.1.24

commit 9d760fc9c0bca9885ce402e0a609fa68599f58b7 (tag: v1.1.24)
Author: Richard Lee <[email protected]>
Date:   Sat Aug 20 07:16:26 2022 -0700

    Adopt existing channel on create (#113)

    * 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot unarchive channel previously destroyed via 'action_on_destroy' = 'archive'
2 participants