├── .ci └── homeserver.yaml ├── .circleci └── config.yml ├── .docker ├── run-tests.sh └── synapse │ ├── homeserver.yaml │ ├── localhost.log.config │ └── localhost.signing.key ├── .dockerignore ├── .gitignore ├── .test_data ├── deadbeef.bin └── words.txt ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── appveyor.yml ├── go.mod ├── go.sum ├── main.go ├── matrix ├── api │ ├── error_codes.go │ ├── event_contents.go │ ├── events.go │ ├── http.go │ ├── request_types.go │ ├── response_types.go │ └── ui_registration.go ├── metadata.go ├── provider.go ├── provider_test.go ├── resource_matrix_content.go ├── resource_matrix_content_test.go ├── resource_matrix_room.go ├── resource_matrix_room_test.go ├── resource_matrix_user.go ├── resource_matrix_user_test.go ├── utils.go └── utils_test.go └── run-tests.sh /.ci/homeserver.yaml: -------------------------------------------------------------------------------- 1 | # This is a test configuration for synapse. It is strongly recommended you do not use this for anything besides 2 | # testing in a CI environment. It is expected that the homeserver will not be exposed beyond localhost, and therefore 3 | # will not be able to persist sessions. 4 | ## TLS ## 5 | 6 | tls_certificate_path: "/data/localhost.tls.crt" 7 | tls_private_key_path: "/data/localhost.tls.key" 8 | tls_dh_params_path: "/data/localhost.tls.dh" 9 | no_tls: False 10 | tls_fingerprints: [] 11 | 12 | ## Server ## 13 | 14 | server_name: "localhost" 15 | pid_file: /homeserver.pid 16 | web_client: False 17 | soft_file_limit: 0 18 | 19 | ## Ports ## 20 | 21 | listeners: 22 | 23 | - 24 | port: 8448 25 | bind_addresses: ['0.0.0.0'] 26 | type: http 27 | tls: true 28 | x_forwarded: false 29 | resources: 30 | - names: [client] 31 | compress: true 32 | - names: [federation] # Federation APIs 33 | compress: false 34 | 35 | 36 | - port: 8008 37 | tls: false 38 | bind_addresses: ['0.0.0.0'] 39 | type: http 40 | x_forwarded: false 41 | 42 | resources: 43 | - names: [client] 44 | compress: true 45 | - names: [federation] 46 | compress: false 47 | 48 | ## Database ## 49 | 50 | 51 | database: 52 | name: "sqlite3" 53 | args: 54 | database: "/data/homeserver.db" 55 | 56 | 57 | ## Performance ## 58 | 59 | event_cache_size: "10K" 60 | verbose: 0 61 | log_file: "/data/homeserver.log" 62 | log_config: "/compiled/log.config" 63 | 64 | ## Ratelimiting ## 65 | 66 | rc_messages_per_second: 0.2 67 | rc_message_burst_count: 10.0 68 | federation_rc_window_size: 1000 69 | federation_rc_sleep_limit: 10 70 | federation_rc_sleep_delay: 500 71 | federation_rc_reject_limit: 50 72 | federation_rc_concurrent: 3 73 | 74 | ## Files ## 75 | 76 | media_store_path: "/data/media" 77 | uploads_path: "/data/uploads" 78 | max_upload_size: "10M" 79 | max_image_pixels: "32M" 80 | dynamic_thumbnails: false 81 | 82 | # List of thumbnail to precalculate when an image is uploaded. 83 | thumbnail_sizes: 84 | - width: 32 85 | height: 32 86 | method: crop 87 | - width: 96 88 | height: 96 89 | method: crop 90 | - width: 320 91 | height: 240 92 | method: scale 93 | - width: 640 94 | height: 480 95 | method: scale 96 | - width: 800 97 | height: 600 98 | method: scale 99 | 100 | url_preview_enabled: False 101 | max_spider_size: "10M" 102 | 103 | ## Captcha ## 104 | 105 | 106 | recaptcha_public_key: "YOUR_PUBLIC_KEY" 107 | recaptcha_private_key: "YOUR_PRIVATE_KEY" 108 | enable_registration_captcha: False 109 | recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" 110 | 111 | 112 | ## Turn ## 113 | 114 | 115 | turn_uris: [] 116 | turn_shared_secret: "YOUR_SHARED_SECRET" 117 | turn_user_lifetime: "1h" 118 | turn_allow_guests: True 119 | 120 | 121 | ## Registration ## 122 | 123 | enable_registration: True 124 | registration_shared_secret: "shared-secret-test1234" 125 | bcrypt_rounds: 12 126 | allow_guest_access: True 127 | enable_group_creation: true 128 | 129 | # The list of identity servers trusted to verify third party 130 | # identifiers by this server. 131 | trusted_third_party_id_servers: 132 | - matrix.org 133 | - vector.im 134 | - riot.im 135 | 136 | ## Metrics ### 137 | 138 | 139 | enable_metrics: False 140 | report_stats: False 141 | 142 | 143 | ## API Configuration ## 144 | 145 | room_invite_state_types: 146 | - "m.room.join_rules" 147 | - "m.room.canonical_alias" 148 | - "m.room.avatar" 149 | - "m.room.name" 150 | 151 | 152 | app_service_config_files: [] 153 | 154 | 155 | macaroon_secret_key: "macaroon-secret-test1234" 156 | expire_access_token: False 157 | 158 | ## Signing Keys ## 159 | 160 | signing_key_path: "/data/localhost.signing.key" 161 | old_signing_keys: {} 162 | key_refresh_interval: "1d" # 1 Day. 163 | 164 | # The trusted servers to download signing keys from. 165 | perspectives: 166 | servers: 167 | "matrix.org": 168 | verify_keys: 169 | "ed25519:auto": 170 | key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" 171 | 172 | password_config: 173 | enabled: true 174 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build_linux_amd64: 4 | environment: 5 | - ARCH: linux-amd64 6 | - GOARCH: amd64 7 | docker: 8 | - image: circleci/golang:1.12 9 | working_directory: /go/github.com/turt2live/terraform-provider-matrix 10 | steps: 11 | - checkout 12 | - run: go install -v ... 13 | - run: go vet 14 | - run: go build -o bin/terraform-provider-matrix-${ARCH} 15 | - store_artifacts: 16 | path: bin/ 17 | build_linux_386: 18 | environment: 19 | - ARCH: linux-386 20 | - GOARCH: 386 21 | docker: 22 | - image: circleci/golang:1.12 23 | working_directory: /go/github.com/turt2live/terraform-provider-matrix 24 | steps: 25 | - checkout 26 | - run: go install -v ... 27 | - run: go vet 28 | - run: go build -o bin/terraform-provider-matrix-${ARCH} 29 | - store_artifacts: 30 | path: bin/ 31 | workflows: 32 | version: 2 33 | build_linux: 34 | jobs: 35 | - build_linux_amd64 36 | - build_linux_386 37 | -------------------------------------------------------------------------------- /.docker/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | go version 4 | go env 5 | 6 | echo "starting synapse" 7 | python /start.py & 8 | SYNAPSE_PID=$! 9 | 10 | echo "waiting for synapse to start" 11 | RETRIES=60 12 | while [ "$RETRIES" -gt 0 ] ; do 13 | sleep 1 14 | curl -sS --connect-timeout 60 http://localhost:8008/_matrix/client/versions && break 15 | let RETRIES=RETRIES-1 16 | done 17 | 18 | echo "creating admin account" 19 | register_new_matrix_user -u admin -p test1234 -a -c /synapse/homeserver.yaml http://localhost:8008 20 | access_token=$(curl -s -H 'Content-Type: application/json' --data '{"type":"m.login.password","user":"admin","password":"test1234"}' http://localhost:8008/_matrix/client/r0/login | jq .access_token | tr -d '"') 21 | export MATRIX_ADMIN_ACCESS_TOKEN=$access_token 22 | export MATRIX_DEFAULT_ACCESS_TOKEN=$access_token 23 | export MATRIX_CLIENT_SERVER_URL="http://localhost:8008" 24 | 25 | echo "preparing project" 26 | cd /project/src/github.com/turt2live/terraform-provider-matrix 27 | cp -rv .test_data /tmp/.test_data 28 | export MATRIX_TEST_DATA_DIR="/tmp" 29 | export TF_ACC=true 30 | #export TF_LOG=DEBUG # Set by the environment variables in the command line 31 | 32 | echo "installing packages" 33 | go install -v ... 34 | 35 | echo "running tests" 36 | go test -v github.com/turt2live/terraform-provider-matrix/matrix 37 | EXIT_CODE=$? 38 | 39 | echo "killing synapse" 40 | kill -9 $SYNAPSE_PID 41 | 42 | echo "done (exit code $EXIT_CODE)" 43 | exit $EXIT_CODE -------------------------------------------------------------------------------- /.docker/synapse/homeserver.yaml: -------------------------------------------------------------------------------- 1 | server_name: "localhost" 2 | pid_file: /homeserver.pid 3 | federation_ip_range_blacklist: 4 | - '0.0.0.0/1' 5 | - '128.0.0.0/2' 6 | - '192.0.0.0/3' 7 | - '224.0.0.0/4' 8 | listeners: 9 | - port: 8008 10 | tls: false 11 | bind_addresses: ['127.0.0.1'] 12 | type: http 13 | x_forwarded: true 14 | resources: 15 | - names: [client, federation] 16 | compress: false 17 | database: 18 | name: "sqlite3" 19 | args: 20 | database: "/synapse/homeserver.db" 21 | log_config: "/synapse/localhost.log.config" 22 | media_store_path: "/synapse/media_store" 23 | uploads_path: "/synapse/uploads" 24 | registration_shared_secret: "registration-shared-secret-test1234" 25 | report_stats: false 26 | macaroon_secret_key: "macaroon-secret-key-test1234" 27 | form_secret: "form-secret-test1234" 28 | signing_key_path: "/synapse/localhost.signing.key" 29 | enable_registration: true 30 | allow_guest_access: true 31 | rc_message: 32 | per_second: 1000000 33 | burst_count: 1000000 34 | rc_registration: 35 | per_second: 1000000 36 | burst_count: 1000000 37 | rc_login: 38 | address: 39 | per_second: 1000000 40 | burst_count: 1000000 41 | account: 42 | per_second: 1000000 43 | burst_count: 1000000 44 | failed_attempts: 45 | per_second: 1000000 46 | burst_count: 1000000 -------------------------------------------------------------------------------- /.docker/synapse/localhost.log.config: -------------------------------------------------------------------------------- 1 | 2 | version: 1 3 | 4 | formatters: 5 | precise: 6 | format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' 7 | 8 | filters: 9 | context: 10 | (): synapse.util.logcontext.LoggingContextFilter 11 | request: "" 12 | 13 | handlers: 14 | console: 15 | class: logging.StreamHandler 16 | formatter: precise 17 | filters: [context] 18 | 19 | loggers: 20 | synapse: 21 | level: INFO 22 | 23 | synapse.storage.SQL: 24 | # beware: increasing this to DEBUG will make synapse log sensitive 25 | # information such as access tokens. 26 | level: INFO 27 | 28 | root: 29 | level: INFO 30 | handlers: [console] 31 | -------------------------------------------------------------------------------- /.docker/synapse/localhost.signing.key: -------------------------------------------------------------------------------- 1 | ed25519 a_jsZb 9j2pxqdXaNd0RofmGAaSmKdP+GXXibtawEgiHNVt+zY 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/terraform-provider-matrix/74a8ed2daf7e99cf103a0b2a52976d73e4aafe96/.dockerignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /bin 3 | /pkg 4 | /logs 5 | 6 | .env 7 | 8 | vendor 9 | terraform-provider-matrix 10 | 11 | # Binaries for programs and plugins 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | 18 | # Test binary, build with `go test -c` 19 | *.test 20 | 21 | # Output of the go coverage tool, specifically when used with LiteIDE 22 | *.out 23 | -------------------------------------------------------------------------------- /.test_data/deadbeef.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turt2live/terraform-provider-matrix/74a8ed2daf7e99cf103a0b2a52976d73e4aafe96/.test_data/deadbeef.bin -------------------------------------------------------------------------------- /.test_data/words.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | World -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: go 3 | go: 4 | - 1.12 5 | env: 6 | # We want to make sure we have as much information as possible when the tests explode 7 | - TF_LOG=DEBUG GO111MODULE=on 8 | services: 9 | - docker 10 | install: 11 | - go install -v ... 12 | - docker build -t terraform-provider-matrix-tests . 13 | script: 14 | - go vet 15 | - docker run --rm --name terraform-provider-matrix-tests terraform-provider-matrix-tests 16 | - go build -o terraform-provider-matrix 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/matrixdotorg/synapse:v1.1.0 2 | 3 | RUN apk add --no-cache gcc musl-dev openssl go curl ca-certificates dos2unix git jq 4 | 5 | RUN wget -O go.tgz https://dl.google.com/go/go1.12.6.src.tar.gz 6 | RUN tar -C /usr/local -xzf go.tgz 7 | WORKDIR /usr/local/go/src/ 8 | RUN sh ./make.bash 9 | ENV GOPATH="/opt/go" 10 | ENV PATH="/usr/local/go/bin:$GOPATH/bin:$PATH" 11 | RUN go version 12 | RUN env 13 | 14 | ENV SYNAPSE_CONFIG_DIR=/synapse 15 | ENV SYNAPSE_CONFIG_PATH=/synapse/homeserver.yaml 16 | ENV UID 991 17 | ENV GID 991 18 | 19 | RUN mkdir -p /synapse 20 | COPY .docker/synapse /synapse 21 | RUN chown -R 991:991 /synapse 22 | 23 | RUN mkdir -p /project/src/github.com/turt2live/terraform-provider-matrix 24 | ENV GOPATH="$GOPATH:/project:/project/src/github.com/turt2live/terraform-provider-matrix/vendor" 25 | ENV GO111MODULE=on 26 | 27 | COPY /.docker/run-tests.sh /run-tests.sh 28 | RUN chmod +x /run-tests.sh && dos2unix /run-tests.sh 29 | 30 | COPY . /project/src/github.com/turt2live/terraform-provider-matrix 31 | 32 | ENTRYPOINT [ "/run-tests.sh" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-provider-matrix 2 | 3 | [![#terraform:t2bot.io](https://img.shields.io/badge/matrix-%23terraform:t2bot.io-brightgreen.svg)](https://matrix.to/#/#terraform:t2bot.io) 4 | [![TravisCI badge](https://travis-ci.org/turt2live/terraform-provider-matrix.svg?branch=master)](https://travis-ci.org/turt2live/terraform-provider-matrix) 5 | [![CircleCI](https://circleci.com/gh/turt2live/terraform-provider-matrix/tree/master.svg?style=svg)](https://circleci.com/gh/turt2live/terraform-provider-matrix/tree/master) 6 | [![AppVeyor badge](https://ci.appveyor.com/api/projects/status/github/turt2live/terraform-provider-matrix?branch=master&svg=true)](https://ci.appveyor.com/project/turt2live/terraform-provider-matrix) 7 | 8 | Terraform your matrix homeserver 9 | 10 | ## Building 11 | 12 | Assuming Go 1.12 is already installed: 13 | ```bash 14 | # Get it 15 | git clone https://github.com/turt2live/terraform-provider-matrix 16 | cd terraform-provider-matrix 17 | 18 | # Install dependencies 19 | go install -v ... 20 | 21 | # Build it 22 | go build -v -o terraform-provider-matrix 23 | ``` 24 | 25 | ## Running the tests 26 | 27 | The tests run within a Docker container. This is to ensure that the test homeserver gets set up correctly and doesn't 28 | leave lingering data on another homeserver. 29 | 30 | The tests can be run with `./run-tests.sh` or by running the following commands: 31 | ``` 32 | docker build -t terraform-provider-matrix-tests . 33 | docker run --rm -it --name terraform-provider-matrix-tests terraform-provider-matrix-tests 34 | ``` 35 | 36 | The first execution may take a while to set up, however future executions should be 37 | fairly quick. 38 | 39 | ## Usage 40 | 41 | The matrix provider is a 3rd party plugin. See the documentation on [3rd party plugins](https://www.terraform.io/docs/configuration/providers.html#third-party-plugins) 42 | for installation instructions, and download the latest release from the [releases page](https://github.com/turt2live/terraform-provider-matrix/releases). 43 | 44 | ```hcl 45 | provider "matrix" { 46 | # The client/server URL to access your matrix homeserver with. 47 | # Environment variable: MATRIX_CLIENT_SERVER_URL 48 | client_server_url = "https://matrix.org" 49 | 50 | # The default access token to use for things like content uploads. 51 | # Does not apply for provisioning users. 52 | # Environment variable: MATRIX_DEFAULT_ACCESS_TOKEN 53 | default_access_token = "MDAxSomeRandomString" 54 | } 55 | ``` 56 | 57 | ## Resources 58 | 59 | The following resources are exposed from this provider. 60 | 61 | ### Media (Content) 62 | 63 | Media (referred to as 'content' in the matrix specification) can be uploaded to the matrix content repository for later 64 | use. Some uses include avatars for users, images in chat, etc. Media can also be existing before entering terraform and 65 | referenced easily (skipping the upload process). Media cannot be deleted or updated. 66 | 67 | Uploading media requires a `default_access_token` to be configured in the provider. 68 | 69 | *Note*: Media cannot be deleted and is therefore abandoned when deleted in Terraform. 70 | 71 | ```hcl 72 | # Existing media 73 | resource "matrix_content" "catpic" { 74 | # Your MXC URI must fit the following format/example: 75 | # Format: mxc://origin/media_id 76 | # Example: mxc://matrix.org/SomeGeneratedId 77 | origin = "matrix.org" 78 | media_id = "SomeGeneratedId" 79 | } 80 | 81 | # New media (upload) 82 | resource "matrix_content" "catpic" { 83 | file_path = "/path/to/cat_pic.png" 84 | file_name = "cat_pic.png" 85 | file_type = "image/png" 86 | } 87 | ``` 88 | 89 | All media will have an `origin` and `media_id` as computed properties. To access the complete MXC URI, use the `id`. 90 | 91 | ### Users 92 | 93 | Users can either be created using a username and password or by providing an access token. Users created with a username 94 | and password will first be registered on the homeserver, and if the username appears to be in use then the provider will 95 | try logging in. 96 | 97 | *Note*: Users cannot be deleted and are therefore abandoned when deleted in Terraform. 98 | 99 | ```hcl 100 | # Username/password user 101 | resource "matrix_user" "foouser" { 102 | username = "foouser" 103 | password = "hunter2" 104 | 105 | # These properties are optional, and will update the user's profile 106 | # We're using a reference to the Media used in an earlier example 107 | display_name = "My Cool User" 108 | avatar_mxc = "${matrix_content.catpic.id}" 109 | } 110 | 111 | # Access token user 112 | resource "matrix_user" "baruser" { 113 | access_token = "MDAxOtherCharactersHere" 114 | 115 | # These properties are optional, and will update the user's profile 116 | # We're using a reference to the Media used in an earlier example 117 | display_name = "My Cool User" 118 | avatar_mxc = "${matrix_content.catpic.id}" 119 | } 120 | ``` 121 | 122 | All users have a `display_name`, `avatar_mxc`, and `access_token` as computed properties. 123 | 124 | ### Rooms 125 | 126 | Rooms can be created by either specifying an explicit `room_id` or by specifying properties that help make up the room's 127 | configuration for a new room. In both cases, a `member_access_token` is required because the provider needs an insight 128 | into the room to perform state checks. 129 | 130 | *Note*: Rooms cannot be completely deleted in matrix. When Terraform deletes a room, this provider will try to make the 131 | room as inaccessible as possible. That generally means ensuring the `join_rules` are set to `private`, everyone is kicked, 132 | aliases are removed, and the creator removes themselves from the room. For this reason, it is recommended that the member's 133 | access token in the resource configuration be of at least power level 100 (Admin). 134 | 135 | The examples here build off of previously mentioned resources, such as Users and Media. 136 | 137 | ```hcl 138 | # Already existing room 139 | resource "matrix_room" "fooroom" { 140 | room_id = "!somewhere:domain.com" 141 | member_access_token = "${matrix_user.foouser.access_token}" 142 | } 143 | 144 | # New room 145 | resource "matrix_room" "barroom" { 146 | creator_user_id = "${matrix_user.foouser.id}" 147 | member_access_token = "${matrix_user.foouser.access_token}" 148 | 149 | # The rest is optional 150 | name = "My Room" 151 | avatar_mxc = "${matrix_content.catpic.id}" 152 | topic = "For testing only please" 153 | preset = "public_chat" 154 | guests_allowed = true 155 | invite_user_ids = ["${matrix_user.baruser.id}"] 156 | local_alias_localpart = "myroom" 157 | } 158 | ``` 159 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | image: Visual Studio 2017 3 | 4 | clone_folder: c:\gopath\github.com\turt2live\teraform-provider-matrix 5 | 6 | environment: 7 | GOPATH: c:\gopath 8 | TF_LOG: DEBUG 9 | matrix: 10 | - GO_DIR: go112 11 | ARCH: amd64 12 | platform: x64 13 | - GO_DIR: go112-x86 14 | ARCH: 386 15 | platform: x86 16 | 17 | max_jobs: 2 18 | 19 | branches: 20 | only: [master, develop] 21 | 22 | init: 23 | - git config --global core.autocrlf input 24 | 25 | install: 26 | - set PATH=c:\%GO_DIR\bin;c:\gopath\bin;%PATH% 27 | - go version 28 | - go env 29 | - go install -v ... 30 | 31 | build_script: 32 | - go vet 33 | - go build -o terraform-provider-matrix-windows-%ARCH%.exe 34 | 35 | # NOTE: We do not run or build the docker image because AppVeyor does not support linux containers. 36 | # We'll use Travis CI to do our tests, and leave AppVeyor to the build/release 37 | #test_script: 38 | #- docker run --rm --name terraform-provider-matrix-tests terraform-provider-matrix-tests 39 | 40 | artifacts: 41 | - path: terraform-provider-matrix-windows-*.exe 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/turt2live/terraform-provider-matrix 2 | 3 | go 1.12 4 | 5 | require github.com/hashicorp/terraform v0.12.3 6 | 7 | replace github.com/go-resty/resty => gopkg.in/resty.v1 v1.12.0 8 | 9 | replace github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.4.2 10 | 11 | replace google.golang.org/cloud => cloud.google.com/go v0.41.0 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.36.0 h1:+aCSj7tOo2LODWVEuZDZeGCckdt6MlSF+X/rB3wUiS8= 5 | cloud.google.com/go v0.36.0/go.mod h1:RUoy9p/M4ge0HzT8L+SDZ8jg+Q6fth0CiBuhFJpSV40= 6 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 7 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 8 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 9 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 10 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 11 | github.com/Azure/azure-sdk-for-go v21.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 12 | github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 13 | github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= 16 | github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= 17 | github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= 18 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 19 | github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= 20 | github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 21 | github.com/agl/ed25519 v0.0.0-20150830182803-278e1ec8e8a6/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= 22 | github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= 23 | github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= 24 | github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= 25 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 26 | github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= 27 | github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M= 28 | github.com/apparentlymart/go-cidr v1.0.0 h1:lGDvXx8Lv9QHjrAVP7jyzleG4F9+FkRhJcEsDFxeb8w= 29 | github.com/apparentlymart/go-cidr v1.0.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= 30 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 31 | github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= 32 | github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 33 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 34 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 35 | github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 36 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 37 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 38 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 39 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 40 | github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= 41 | github.com/aws/aws-sdk-go v1.16.36/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 42 | github.com/aws/aws-sdk-go v1.20.4 h1:czX3oqFyqz/AELrK/tneNuyZgNIrWnyqP+iQXsQ32E0= 43 | github.com/aws/aws-sdk-go v1.20.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 44 | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= 45 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 46 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= 47 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= 48 | github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= 49 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 50 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= 51 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 52 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 53 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 54 | github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= 55 | github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= 56 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 57 | github.com/chzyer/readline v0.0.0-20161106042343-c914be64f07d/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 58 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 59 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 60 | github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 61 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 62 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 63 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 64 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 65 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 67 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 69 | github.com/dimchansky/utfbom v1.0.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= 70 | github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= 71 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 72 | github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= 73 | github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y= 74 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 75 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 76 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 77 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 78 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 79 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 80 | github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= 81 | github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 82 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 83 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 84 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 85 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 86 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 87 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 88 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 89 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 90 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 91 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 92 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 94 | github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= 95 | github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= 96 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 97 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 98 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 99 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 100 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 101 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 102 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 103 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 104 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 105 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 106 | github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= 107 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 108 | github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mqJt0Cc= 109 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 110 | github.com/gophercloud/gophercloud v0.0.0-20190208042652-bc37892e1968/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 111 | github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw= 112 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 113 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 114 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 115 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 116 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 117 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 118 | github.com/grpc-ecosystem/grpc-gateway v1.5.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 119 | github.com/hashicorp/aws-sdk-go-base v0.2.0/go.mod h1:ZIWACGGi0N7a4DZbf15yuE1JQORmWLtBcVM6F5SXNFU= 120 | github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= 121 | github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 122 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 123 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 124 | github.com/hashicorp/go-azure-helpers v0.0.0-20190129193224-166dfd221bb2/go.mod h1:lu62V//auUow6k0IykxLK2DCNW8qTmpm8KqhYVWattA= 125 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 126 | github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= 127 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 128 | github.com/hashicorp/go-getter v1.3.0 h1:pFMSFlI9l5NaeuzkpE3L7BYk9qQ9juTAgXW/H0cqxcU= 129 | github.com/hashicorp/go-getter v1.3.0/go.mod h1:/O1k/AizTN0QmfEKknCYGvICeyKUDqCYA8vvWtGWDeQ= 130 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 131 | github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f h1:Yv9YzBlAETjy6AOX9eLBZ3nshNVRREgerT/3nvxlGho= 132 | github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 133 | github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw= 134 | github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 135 | github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= 136 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 137 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 138 | github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 h1:hRho44SAoNu1CBtn5r8Q9J3rCs4ZverWZ4R+UeeNuWM= 139 | github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= 140 | github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 141 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 142 | github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= 143 | github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= 144 | github.com/hashicorp/go-slug v0.3.0/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= 145 | github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 146 | github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM= 147 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 148 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 149 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 150 | github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= 151 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 152 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 153 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= 154 | github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 155 | github.com/hashicorp/hcl2 v0.0.0-20181208003705-670926858200/go.mod h1:ShfpTh661oAaxo7VcNxg0zcZW6jvMa7Moy2oFx7e5dE= 156 | github.com/hashicorp/hcl2 v0.0.0-20190618163856-0b64543c968c h1:P96avlEdjyi6kpx6kTbTbuQb5GuZvVTrLK9FWKwTy6A= 157 | github.com/hashicorp/hcl2 v0.0.0-20190618163856-0b64543c968c/go.mod h1:FSQTwDi9qesxGBsII2VqhIzKQ4r0bHvBkOczWfD7llg= 158 | github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI= 159 | github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= 160 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 161 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 162 | github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= 163 | github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= 164 | github.com/hashicorp/terraform v0.12.3 h1:6pdvXmXgUOQDonbwNNQfV113/D6qZ7dt57UJ0Pp6Woc= 165 | github.com/hashicorp/terraform v0.12.3/go.mod h1:qkOSh7ytFDM6avMkFZErKhE810aB/8J+qhIEt5nRifg= 166 | github.com/hashicorp/terraform-config-inspect v0.0.0-20190327195015-8022a2663a70 h1:oZm5nE11yhzsTRz/YrUyDMSvixePqjoZihwn8ipuOYI= 167 | github.com/hashicorp/terraform-config-inspect v0.0.0-20190327195015-8022a2663a70/go.mod h1:ItvqtvbC3K23FFET62ZwnkwtpbKZm8t8eMcWjmVVjD8= 168 | github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0= 169 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= 170 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 171 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 172 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 173 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 174 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 175 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 176 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 177 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 178 | github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= 179 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 180 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 181 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 182 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 183 | github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= 184 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 185 | github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 186 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 187 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 188 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 189 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 190 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 191 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 192 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 193 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 194 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 195 | github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= 196 | github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= 197 | github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= 198 | github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b/go.mod h1:wr1VqkwW0AB5JS0QLy5GpVMS9E3VtRoSYXUYyVk46KY= 199 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 200 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 201 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 202 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 203 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 204 | github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= 205 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 206 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 207 | github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= 208 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 209 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 210 | github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 211 | github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= 212 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 213 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 214 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 215 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= 216 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 217 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 218 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 219 | github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= 220 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 221 | github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= 222 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 223 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 224 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 225 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 226 | github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y= 227 | github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= 228 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 229 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 230 | github.com/mitchellh/panicwrap v0.0.0-20190213213626-17011010aaa4/go.mod h1:YYMf4xtQnR8LRC0vKi3afvQ5QwRPQ17zjcpkBCufb+I= 231 | github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo= 232 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 233 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 234 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 235 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 236 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 237 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 238 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 239 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 240 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 241 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 242 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 243 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 244 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 245 | github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= 246 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 247 | github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 248 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 249 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 250 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 251 | github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI= 252 | github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= 253 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 254 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 255 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 256 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 257 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 258 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 259 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 260 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 261 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 262 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 263 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 264 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 265 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 266 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 267 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 268 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 269 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 270 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 271 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 272 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 273 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 274 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 275 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 276 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 277 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 278 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 279 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 280 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 281 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 282 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 283 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 284 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 285 | github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= 286 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 287 | github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 288 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 289 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 290 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 291 | github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= 292 | github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 293 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 294 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 295 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 296 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 297 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 298 | github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw= 299 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 300 | github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew= 301 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 302 | github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 303 | github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= 304 | github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 305 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 306 | github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= 307 | github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 308 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 309 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 310 | github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= 311 | github.com/zclconf/go-cty v0.0.0-20181129180422-88fbe721e0f8/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 312 | github.com/zclconf/go-cty v0.0.0-20190516203816-4fecf87372ec/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 313 | github.com/zclconf/go-cty v1.0.0 h1:EWtv3gKe2wPLIB9hQRQJa7k/059oIfAqcEkCNnaVckk= 314 | github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 315 | github.com/zclconf/go-cty-yaml v0.1.0 h1:OP5nkApyAuXB88t8mRUqxD9gbKZocSLuVovrBAt8z10= 316 | github.com/zclconf/go-cty-yaml v0.1.0/go.mod h1:Lk26EcRlO3XbaQ8U2fxIJbEtbgEteSZFUpEr3XFTtsU= 317 | go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= 318 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 319 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 320 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 321 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 322 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 323 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 324 | golang.org/x/crypto v0.0.0-20180816225734-aabede6cba87/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 325 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 326 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 327 | golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 328 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 329 | golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 330 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 331 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= 332 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 333 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 334 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 335 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 336 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 337 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 338 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 339 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 340 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 341 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 342 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 343 | golang.org/x/net v0.0.0-20181129055619-fae4c4e3ad76/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 344 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 345 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 346 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 347 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 348 | golang.org/x/net v0.0.0-20190502183928-7f726cade0ab h1:9RfW3ktsOZxgo9YNbBAjq1FWzc/igwEcUzZz8IXgSbk= 349 | golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 350 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 351 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 352 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 353 | golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9 h1:pfyU+l9dEu0vZzDDMsdAKa1gZbJYEn6urYXj/+Xkz7s= 354 | golang.org/x/oauth2 v0.0.0-20190220154721-9b3c75971fc9/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 355 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 356 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 358 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 359 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 360 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 361 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 362 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 363 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 364 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 365 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 366 | golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 367 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 368 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 369 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 370 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 371 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w= 373 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 375 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 376 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 377 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 378 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 379 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 380 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 381 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 382 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 383 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 384 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 385 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 386 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 387 | google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI= 388 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 389 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 390 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 391 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 392 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 393 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 394 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 395 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 396 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 397 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 398 | google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 h1:mBVYJnbrXLA/ZCBTCe7PtEgAUP+1bg92qTaFoPHdz+8= 399 | google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= 400 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 401 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 402 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 403 | google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA= 404 | google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 405 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 406 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 407 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 408 | gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 409 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 410 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 411 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 412 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 413 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 414 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 415 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 416 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 417 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 418 | howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 419 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 420 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 421 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/plugin" 5 | "github.com/turt2live/terraform-provider-matrix/matrix" 6 | ) 7 | 8 | func main() { 9 | plugin.Serve(&plugin.ServeOpts{ 10 | ProviderFunc: matrix.Provider, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /matrix/api/error_codes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | const ErrCodeUnknownToken = "M_UNKNOWN_TOKEN" 4 | const ErrCodeUserInUse = "M_USER_IN_USE" 5 | const ErrCodeNotFound = "M_NOT_FOUND" -------------------------------------------------------------------------------- /matrix/api/event_contents.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type RoomNameEventContent struct { 4 | Name string `json:"name"` 5 | } 6 | 7 | type RoomTopicEventContent struct { 8 | Topic string `json:"topic"` 9 | } 10 | 11 | type RoomAvatarEventContent struct { 12 | AvatarMxc string `json:"url"` 13 | } 14 | 15 | type RoomMemberEventContent struct { 16 | DisplayName string `json:"displayname,omitempty"` 17 | AvatarMxc string `json:"avatar_url,omitempty"` 18 | Membership string `json:"membership"` 19 | } 20 | 21 | type RoomGuestAccessEventContent struct { 22 | Policy string `json:"guest_access"` 23 | } 24 | 25 | type RoomCreateEventContent struct { 26 | CreatorUserId string `json:"creator"` 27 | } 28 | 29 | type RoomJoinRulesEventContent struct { 30 | Policy string `json:"join_rule"` 31 | } 32 | 33 | type RoomAliasesEventContent struct { 34 | Aliases []string `json:"aliases,flow"` 35 | } 36 | -------------------------------------------------------------------------------- /matrix/api/events.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type RoomMemberEvent struct { 4 | Content *RoomMemberEventContent `json:"content"` 5 | Type string `json:"type"` 6 | EventId string `json:"event_id"` 7 | RoomId string `json:"room_id"` 8 | StateKey string `json:"state_key"` 9 | 10 | // other fields not included 11 | } 12 | -------------------------------------------------------------------------------- /matrix/api/http.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | "bytes" 7 | "io/ioutil" 8 | "encoding/json" 9 | "net/url" 10 | "fmt" 11 | "io" 12 | "log" 13 | ) 14 | 15 | var matrixHttpClient = &http.Client{ 16 | Timeout: 30 * time.Second, 17 | } 18 | 19 | // Based in part on https://github.com/matrix-org/gomatrix/blob/072b39f7fa6b40257b4eead8c958d71985c28bdd/client.go#L180-L243 20 | func DoRequest(method string, urlStr string, body interface{}, result interface{}, accessToken string) (error) { 21 | var bodyBytes []byte 22 | if body != nil { 23 | jsonStr, err := json.Marshal(body) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | bodyBytes = jsonStr 29 | } 30 | 31 | return doRawRequest(method, urlStr, bodyBytes, "application/json", result, accessToken) 32 | } 33 | 34 | func UploadFile(csApiUrl string, content []byte, name string, mime string, accessToken string) (*ContentUploadResponse, error) { 35 | qs := make(map[string]string) 36 | if name != "" { 37 | qs["filename"] = name 38 | } 39 | urlStr := MakeUrlQueryString(qs, csApiUrl, "/_matrix/media/r0/upload") 40 | log.Println("[DEBUG] Performing upload:", urlStr) 41 | result := &ContentUploadResponse{} 42 | err := doRawRequest("POST", urlStr, content, mime, result, accessToken) 43 | return result, err 44 | } 45 | 46 | func DownloadFile(csApiUrl string, origin string, mediaId string) (*io.ReadCloser, http.Header, error) { 47 | urlStr := MakeUrl(csApiUrl, "/_matrix/media/r0/download", origin, mediaId) 48 | log.Println("[DEBUG] Performing download:", urlStr) 49 | req, err := http.NewRequest("GET", urlStr, nil) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | res, err := matrixHttpClient.Do(req) 55 | if err != nil { 56 | return &res.Body, res.Header, err 57 | } 58 | 59 | if res.StatusCode != http.StatusOK { 60 | return &res.Body, res.Header, fmt.Errorf("request failed: status code %d", res.StatusCode) 61 | } 62 | 63 | return &res.Body, res.Header, nil 64 | } 65 | 66 | func doRawRequest(method string, urlStr string, bodyBytes []byte, contentType string, result interface{}, accessToken string) (error) { 67 | log.Println("[DEBUG]", method, urlStr) 68 | req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(bodyBytes)) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | req.Header.Set("Content-Type", contentType) 74 | if accessToken != "" { 75 | req.Header.Set("Authorization", "Bearer "+accessToken) 76 | } 77 | 78 | res, err := matrixHttpClient.Do(req) 79 | if res != nil { 80 | defer res.Body.Close() 81 | } 82 | if err != nil { 83 | return err 84 | } 85 | 86 | contents, err := ioutil.ReadAll(res.Body) 87 | if err != nil { 88 | return err 89 | } 90 | if res.StatusCode != http.StatusOK { 91 | mtxErr := &ErrorResponse{} 92 | mtxErr.RawError = string(contents) 93 | mtxErr.StatusCode = res.StatusCode 94 | err = json.Unmarshal(contents, mtxErr) 95 | if err != nil { 96 | return fmt.Errorf("request failed: %s", string(contents)) 97 | } 98 | return mtxErr 99 | } 100 | 101 | if result != nil { 102 | err = json.Unmarshal(contents, &result) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func MakeUrl(parts ... string) string { 112 | res := "" 113 | for i, p := range parts { 114 | if p[len(p)-1:] == "/" { 115 | res += p[:len(p)-1] 116 | } else if p[0] != '/' && i > 0 { 117 | res += "/" + p 118 | } else { 119 | res += p 120 | } 121 | } 122 | return res 123 | } 124 | 125 | func MakeUrlQueryString(query map[string]string, parts ... string) string { 126 | urlStr := MakeUrl(parts...) 127 | 128 | u, _ := url.Parse(urlStr) 129 | q := u.Query() 130 | for k, v := range query { 131 | q.Set(k, v) 132 | } 133 | u.RawQuery = q.Encode() 134 | 135 | return u.String() 136 | } 137 | -------------------------------------------------------------------------------- /matrix/api/request_types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type RegisterRequest struct { 4 | Authentication *RegisterAuthenticationData `json:"auth,omitempty"` 5 | BindEmail bool `json:"bind_email,omitempty"` 6 | Username string `json:"username,omitempty"` 7 | Password string `json:"password,omitempty"` 8 | DeviceId string `json:"device_id,omitempty"` 9 | InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` 10 | } 11 | 12 | type RegisterAuthenticationData struct { 13 | Type string `json:"type"` 14 | Session string `json:"session"` 15 | } 16 | 17 | const LoginTypePassword = "m.login.password" 18 | const LoginTypeToken = "m.login.token" 19 | 20 | type LoginRequest struct { 21 | Type string `json:"type"` 22 | Username string `json:"user,omitempty"` 23 | Password string `json:"password,omitempty"` 24 | // ... and other parameters we don't care about 25 | } 26 | 27 | type ProfileDisplayNameRequest struct { 28 | DisplayName string `json:"displayname,omitempty"` 29 | } 30 | 31 | type ProfileAvatarUrlRequest struct { 32 | AvatarMxc string `json:"avatar_url,omitempty"` 33 | } 34 | 35 | type CreateRoomRequest struct { 36 | Visibility string `json:"visibility,omitempty"` 37 | AliasLocalpart string `json:"room_alias_name,omitempty"` 38 | InviteUserIds []string `json:"invite,flow,omitempty"` 39 | CreationContent map[string]interface{} `json:"creation_content,omitempty"` 40 | InitialState []CreateRoomStateEvent `json:"initial_state,flow,omitempty"` 41 | Preset string `json:"preset,omitempty"` 42 | IsDirect bool `json:"is_direct"` 43 | } 44 | 45 | type CreateRoomStateEvent struct { 46 | Type string `json:"type"` 47 | StateKey string `json:"state_key"` 48 | Content interface{} `json:"content"` 49 | } 50 | 51 | type KickRequest struct { 52 | UserId string `json:"user_id"` 53 | Reason string `json:"reason,omitempty"` 54 | } 55 | -------------------------------------------------------------------------------- /matrix/api/response_types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ErrorResponse struct { 8 | ErrorCode string `json:"errcode"` 9 | Message string `json:"error"` 10 | RawError string 11 | StatusCode int 12 | } 13 | 14 | func (e ErrorResponse) Error() string { 15 | return fmt.Sprintf("code=%s message=%s raw=%s status_code=%d", e.ErrorCode, e.Message, e.RawError, e.StatusCode) 16 | } 17 | 18 | type RegisterResponse struct { 19 | UserId string `json:"user_id"` 20 | AccessToken string `json:"access_token"` 21 | DeviceId string `json:"device_id"` 22 | 23 | // home_server is deprecated and therefore not included 24 | } 25 | 26 | type LoginResponse struct { 27 | UserId string `json:"user_id"` 28 | AccessToken string `json:"access_token"` 29 | DeviceId string `json:"device_id"` 30 | 31 | // home_server is deprecated and therefore not included 32 | } 33 | 34 | type ProfileResponse struct { 35 | DisplayName string `json:"displayname"` 36 | AvatarMxc string `json:"avatar_url"` 37 | } 38 | 39 | type WhoAmIResponse struct { 40 | UserId string `json:"user_id"` 41 | } 42 | 43 | type AdminWhoisResponse struct { 44 | UserId string `json:"user_id"` 45 | } 46 | 47 | type UiAuthResponse struct { 48 | Session string `json:"session"` 49 | Flows []*UiAuthFlow `json:"flows,flow"` 50 | Completed *[]string `json:"completed,flow"` 51 | Params map[string]interface{} `json:"params"` 52 | } 53 | 54 | type UiAuthFlow struct { 55 | Stages []string `json:"stages,flow"` 56 | } 57 | 58 | type ProfileUpdateResponse struct { 59 | // There isn't actually anything here 60 | } 61 | 62 | type ContentUploadResponse struct { 63 | ContentMxc string `json:"content_uri"` 64 | } 65 | 66 | type RoomIdResponse struct { 67 | RoomId string `json:"room_id"` 68 | } 69 | 70 | type EventIdResponse struct { 71 | EventId string `json:"event_id"` 72 | } 73 | 74 | type RoomDirectoryLookupResponse struct { 75 | RoomId string `json:"room_id"` 76 | Servers []string `json:"servers,flow"` 77 | } 78 | 79 | type RoomMembersResponse struct { 80 | Chunk []RoomMemberEvent `json:"chunk,flow"` 81 | } 82 | -------------------------------------------------------------------------------- /matrix/api/ui_registration.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | ) 9 | 10 | const AuthTypeDummy = "m.login.dummy" 11 | 12 | func DoRegister(csApiUrl string, username string, password string, kind string) (*RegisterResponse, error) { 13 | qs := map[string]string{"kind": kind} 14 | urlStr := MakeUrlQueryString(qs, csApiUrl, "/_matrix/client/r0/register") 15 | 16 | // First we do a request to get the flows we can use 17 | log.Println("[DEBUG] Getting registration flows") 18 | request := &RegisterRequest{} 19 | state, _, err := doUiAuthRegisterRequest(urlStr, request) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | // Now that we have the process started, make sure we can actually follow one of these methods 25 | hasDummyStage := false 26 | for _, flow := range state.Flows { 27 | if len(flow.Stages) == 1 && flow.Stages[0] == AuthTypeDummy { 28 | hasDummyStage = true 29 | break 30 | } 31 | } 32 | if !hasDummyStage { 33 | return nil, errors.New("no dummy auth stage") 34 | } 35 | 36 | // We have a dummy stage, so we can expect to be able to register now 37 | log.Println("[DEBUG] Using dummy registration flow to register user") 38 | request = &RegisterRequest{ 39 | Authentication: &RegisterAuthenticationData{ 40 | Type: AuthTypeDummy, 41 | Session: state.Session, 42 | }, 43 | Username: username, 44 | Password: password, 45 | } 46 | _, response, err := doUiAuthRegisterRequest(urlStr, request) 47 | if err != nil { 48 | return nil, err 49 | } 50 | if response == nil { 51 | return nil, errors.New("ui auth failed: expected response but got login flow") 52 | } 53 | 54 | return response, nil 55 | } 56 | 57 | func doUiAuthRegisterRequest(urlStr string, request *RegisterRequest) (*UiAuthResponse, *RegisterResponse, error) { 58 | response := &RegisterResponse{} 59 | err := DoRequest("POST", urlStr, request, response, "") 60 | if err != nil { 61 | if r, ok := err.(*ErrorResponse); ok { 62 | if r.StatusCode == http.StatusUnauthorized { 63 | authState := &UiAuthResponse{} 64 | err2 := json.Unmarshal([]byte(r.RawError), authState) 65 | if err2 != nil { 66 | return nil, nil, err2 67 | } 68 | 69 | return authState, nil, nil 70 | } 71 | } 72 | 73 | return nil, nil, err 74 | } 75 | 76 | return nil, response, nil 77 | } 78 | -------------------------------------------------------------------------------- /matrix/metadata.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | type Metadata struct { 4 | ClientApiUrl string 5 | DefaultAccessToken string 6 | } 7 | -------------------------------------------------------------------------------- /matrix/provider.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/terraform" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func Provider() terraform.ResourceProvider { 9 | return &schema.Provider{ 10 | Schema: map[string]*schema.Schema{ 11 | "client_server_url": { 12 | Type: schema.TypeString, 13 | Required: true, 14 | DefaultFunc: schema.EnvDefaultFunc("MATRIX_CLIENT_SERVER_URL", nil), 15 | Description: "The URL for your matrix homeserver. Eg: https://matrix.org", 16 | }, 17 | "default_access_token": { 18 | Type: schema.TypeString, 19 | Optional: true, 20 | DefaultFunc: schema.EnvDefaultFunc("MATRIX_DEFAULT_ACCESS_TOKEN", ""), 21 | Description: "The default access token to use for miscellaneous requests (media uploads, etc)", 22 | }, 23 | }, 24 | 25 | ResourcesMap: map[string]*schema.Resource{ 26 | "matrix_user": resourceUser(), 27 | "matrix_content": resourceContent(), 28 | "matrix_room": resourceRoom(), 29 | }, 30 | 31 | ConfigureFunc: providerConfigure, 32 | } 33 | } 34 | 35 | func providerConfigure(d *schema.ResourceData) (interface{}, error) { 36 | config := Metadata{ 37 | ClientApiUrl: d.Get("client_server_url").(string), 38 | DefaultAccessToken: d.Get("default_access_token").(string), 39 | } 40 | 41 | return config, nil 42 | } 43 | -------------------------------------------------------------------------------- /matrix/provider_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/helper/schema" 5 | "github.com/hashicorp/terraform/terraform" 6 | "os" 7 | "testing" 8 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 9 | "log" 10 | ) 11 | 12 | type test_MatrixUser struct { 13 | Localpart string 14 | Password string 15 | AccessToken string 16 | UserId string 17 | DisplayName string 18 | AvatarMxc string 19 | } 20 | 21 | var test_MatrixUser_users = make(map[string]*test_MatrixUser) 22 | var testAccProviders map[string]terraform.ResourceProvider 23 | var testAccProvider *schema.Provider 24 | 25 | func init() { 26 | testAccProvider = Provider().(*schema.Provider) 27 | testAccProviders = map[string]terraform.ResourceProvider{ 28 | "matrix": testAccProvider, 29 | } 30 | } 31 | 32 | func TestProvider(t *testing.T) { 33 | if err := Provider().(*schema.Provider).InternalValidate(); err != nil { 34 | t.Fatalf("err: %s", err) 35 | } 36 | } 37 | 38 | func TestProvider_impl(t *testing.T) { 39 | var _ terraform.ResourceProvider = Provider() 40 | } 41 | 42 | func testAccPreCheck(t *testing.T) { 43 | if v := os.Getenv("MATRIX_CLIENT_SERVER_URL"); v == "" { 44 | t.Fatal("MATRIX_CLIENT_SERVER_URL must be set for acceptance tests") 45 | } 46 | if v := os.Getenv("MATRIX_ADMIN_ACCESS_TOKEN"); v == "" { 47 | t.Fatal("MATRIX_ADMIN_ACCESS_TOKEN must be set for acceptance tests") 48 | } 49 | if v := os.Getenv("MATRIX_DEFAULT_ACCESS_TOKEN"); v == "" { 50 | t.Fatal("MATRIX_DEFAULT_ACCESS_TOKEN must be set for acceptance tests") 51 | } 52 | } 53 | 54 | func testAccTestDataDir() string { 55 | return os.Getenv("MATRIX_TEST_DATA_DIR") 56 | } 57 | 58 | func testAccClientServerUrl() string { 59 | return os.Getenv("MATRIX_CLIENT_SERVER_URL") 60 | } 61 | 62 | func testAccAdminToken() string { 63 | return os.Getenv("MATRIX_ADMIN_ACCESS_TOKEN") 64 | } 65 | 66 | func testAccCreateTestUser(localpart string) (*test_MatrixUser) { 67 | existing := test_MatrixUser_users[localpart] 68 | if existing != nil { 69 | log.Println("[DEBUG] User already exists, returning cached copy:", localpart) 70 | return existing 71 | } 72 | 73 | csApiUrl := testAccProvider.Meta().(Metadata).ClientApiUrl 74 | password := "test1234" 75 | displayName := "!!TEST USER!!" 76 | avatarMxc := "mxc://domain.com/SomeAvatarUrl" 77 | 78 | log.Println("[DEBUG] Attempting to register user:", localpart) 79 | r, e := api.DoRegister(csApiUrl, localpart, password, "user") 80 | if e != nil { 81 | panic(e) 82 | } 83 | 84 | log.Println("[DEBUG] Updating profile for:", localpart) 85 | response := &api.ProfileUpdateResponse{} 86 | nameRequest := &api.ProfileDisplayNameRequest{DisplayName: displayName} 87 | urlStr := api.MakeUrl(csApiUrl, "/_matrix/client/r0/profile/", r.UserId, "/displayname") 88 | e = api.DoRequest("PUT", urlStr, nameRequest, response, r.AccessToken) 89 | if e != nil { 90 | panic(e) 91 | } 92 | 93 | avatarRequest := &api.ProfileAvatarUrlRequest{AvatarMxc: avatarMxc} 94 | urlStr = api.MakeUrl(csApiUrl, "/_matrix/client/r0/profile/", r.UserId, "/avatar_url") 95 | e = api.DoRequest("PUT", urlStr, avatarRequest, response, r.AccessToken) 96 | if e != nil { 97 | panic(e) 98 | } 99 | 100 | existing = &test_MatrixUser{ 101 | Localpart: localpart, 102 | Password: password, 103 | AccessToken: r.AccessToken, 104 | UserId: r.UserId, 105 | DisplayName: displayName, 106 | AvatarMxc: avatarMxc, 107 | } 108 | 109 | test_MatrixUser_users[localpart] = existing 110 | return existing 111 | } 112 | -------------------------------------------------------------------------------- /matrix/resource_matrix_content.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/helper/schema" 5 | "fmt" 6 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "log" 11 | ) 12 | 13 | func resourceContent() *schema.Resource { 14 | return &schema.Resource{ 15 | Exists: resourceContentExists, 16 | Create: resourceContentCreate, 17 | Read: resourceContentRead, 18 | //Update: resourceContentUpdate, // We can't update media, and everything is ForceNew 19 | Delete: resourceContentDelete, 20 | 21 | Schema: map[string]*schema.Schema{ 22 | "origin": { 23 | Type: schema.TypeString, 24 | Computed: true, 25 | Optional: true, 26 | ForceNew: true, 27 | }, 28 | "media_id": { 29 | Type: schema.TypeString, 30 | Computed: true, 31 | Optional: true, 32 | ForceNew: true, 33 | }, 34 | "file_path": { 35 | Type: schema.TypeString, 36 | Optional: true, 37 | ForceNew: true, 38 | }, 39 | "file_type": { 40 | Type: schema.TypeString, 41 | Optional: true, 42 | ForceNew: true, 43 | }, 44 | "file_name": { 45 | Type: schema.TypeString, 46 | Optional: true, 47 | ForceNew: true, 48 | }, 49 | }, 50 | } 51 | } 52 | 53 | func resourceContentCreate(d *schema.ResourceData, m interface{}) error { 54 | meta := m.(Metadata) 55 | 56 | originRaw := nilIfEmptyString(d.Get("origin")) 57 | mediaIdRaw := nilIfEmptyString(d.Get("media_id")) 58 | filePathRaw := nilIfEmptyString(d.Get("file_path")) 59 | fileTypeRaw := nilIfEmptyString(d.Get("file_type")) 60 | fileNameRaw := nilIfEmptyString(d.Get("file_name")) 61 | 62 | if (originRaw != nil && mediaIdRaw == nil) || (originRaw == nil && mediaIdRaw != nil) { 63 | return fmt.Errorf("both the media_id and origin must be supplied") 64 | } 65 | 66 | var mxcRaw interface{} 67 | if originRaw != nil { 68 | mxcRaw = fmt.Sprintf("mxc://%s/%s", originRaw, mediaIdRaw) 69 | } 70 | 71 | if mxcRaw != nil && (filePathRaw != nil || fileTypeRaw != nil || fileNameRaw != nil) { 72 | return fmt.Errorf("origin and media_id cannot be provided alongside file information") 73 | } 74 | if mxcRaw == nil && filePathRaw == nil { 75 | return fmt.Errorf("file_path must be supplied or an origin with media_id") 76 | } 77 | 78 | if mxcRaw != nil { 79 | mxc, origin, mediaId, err := stripMxc(mxcRaw.(string)) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if origin != originRaw { 85 | return fmt.Errorf("origin mismatch while creating object. expected: '%s' got: '%s'", originRaw, origin) 86 | } 87 | if mediaId != mediaIdRaw { 88 | return fmt.Errorf("media_id mismatch while creating object. expected: '%s' got: '%s'", mediaIdRaw, mediaId) 89 | } 90 | 91 | log.Println("[DEBUG] Creating media object from existing parameters - no upload required") 92 | 93 | d.SetId(mxc) 94 | d.Set("origin", origin) 95 | d.Set("media_id", mediaId) 96 | } else { 97 | if meta.DefaultAccessToken == "" { 98 | return fmt.Errorf("a default access token is required to upload content") 99 | } 100 | 101 | log.Println("[DEBUG] Uploading media to create media object") 102 | 103 | f, err := os.Open(filePathRaw.(string)) 104 | if err != nil { 105 | return fmt.Errorf("error opening file: %s", err) 106 | } 107 | defer f.Close() 108 | 109 | contentBytes, err := ioutil.ReadAll(f) 110 | if err != nil { 111 | return fmt.Errorf("error reading file: %s", err) 112 | } 113 | 114 | fileName := "" 115 | if fileNameRaw != nil { 116 | fileName = fileNameRaw.(string) 117 | } 118 | 119 | contentType := "application/octet-stream" 120 | if fileTypeRaw != nil { 121 | contentType = fileTypeRaw.(string) 122 | } 123 | 124 | result, err := api.UploadFile(meta.ClientApiUrl, contentBytes, fileName, contentType, meta.DefaultAccessToken) 125 | if err != nil { 126 | return fmt.Errorf("error uploading content: %s", err) 127 | } 128 | 129 | mxc, origin, mediaId, err := stripMxc(result.ContentMxc) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | d.SetId(mxc) 135 | d.Set("origin", origin) 136 | d.Set("media_id", mediaId) 137 | } 138 | 139 | log.Println("[DEBUG] MXC URI =", d.Id()) 140 | return resourceContentRead(d, meta) 141 | } 142 | 143 | func resourceContentExists(d *schema.ResourceData, m interface{}) (bool, error) { 144 | meta := m.(Metadata) 145 | 146 | origin := d.Get("origin").(string) 147 | mediaId := d.Get("media_id").(string) 148 | 149 | log.Println("[DEBUG] Checking to see if media exists") 150 | stream, _, err := api.DownloadFile(meta.ClientApiUrl, origin, mediaId) 151 | if stream != nil { 152 | defer (*stream).Close() 153 | io.Copy(ioutil.Discard, *stream) 154 | } 155 | if err != nil { 156 | log.Println("[DEBUG] Error downloading meda, assuming deleted:", err) 157 | return false, nil 158 | } 159 | 160 | return true, nil 161 | } 162 | 163 | func resourceContentRead(d *schema.ResourceData, m interface{}) error { 164 | filePathRaw := nilIfEmptyString(d.Get("file_path")) 165 | if filePathRaw == nil { 166 | d.Set("file_path", "") 167 | d.Set("file_type", "") 168 | d.Set("file_name", "") 169 | } 170 | return nil 171 | } 172 | 173 | func resourceContentDelete(d *schema.ResourceData, m interface{}) error { 174 | // Content cannot be deleted in matrix (yet), so we just fake it 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /matrix/resource_matrix_content_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | "github.com/hashicorp/terraform/helper/resource" 6 | "fmt" 7 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 8 | "github.com/hashicorp/terraform/terraform" 9 | "regexp" 10 | "io" 11 | "io/ioutil" 12 | "mime" 13 | "os" 14 | "path" 15 | ) 16 | 17 | type testAccMatrixContentUpload struct { 18 | Mxc string 19 | Origin string 20 | MediaId string 21 | Content []byte 22 | FilePath string 23 | FileName string 24 | FileType string 25 | } 26 | 27 | func testAccCreateMatrixContent(content []byte, mime string, fileName string) (*testAccMatrixContentUpload) { 28 | response, err := api.UploadFile(testAccClientServerUrl(), content, fileName, mime, testAccAdminToken()) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | mxc, origin, mediaId, err := stripMxc(response.ContentMxc) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | return &testAccMatrixContentUpload{ 39 | Mxc: mxc, 40 | Origin: origin, 41 | MediaId: mediaId, 42 | Content: content, 43 | FilePath: "", 44 | FileName: fileName, 45 | FileType: mime, 46 | } 47 | } 48 | 49 | var testAccMatrixContentConfig_existingContent = ` 50 | resource "matrix_content" "foobar" { 51 | origin = "%s" 52 | media_id = "%s" 53 | }` 54 | 55 | func TestAccMatrixContent_ExistingContent(t *testing.T) { 56 | upload := testAccCreateMatrixContent([]byte("hello world"), "text/plain", "hello.txt") 57 | conf := fmt.Sprintf(testAccMatrixContentConfig_existingContent, upload.Origin, upload.MediaId) 58 | 59 | resource.Test(t, resource.TestCase{ 60 | PreCheck: func() { testAccPreCheck(t) }, 61 | Providers: testAccProviders, 62 | // We don't check if content get destroyed because it isn't 63 | //CheckDestroy: testAccCheckMatrixContentDestroy, 64 | Steps: []resource.TestStep{ 65 | { 66 | Config: conf, 67 | Check: resource.ComposeTestCheckFunc( 68 | testAccCheckMatrixContentExists("matrix_content.foobar"), 69 | testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 70 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 71 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 72 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 73 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 74 | ), 75 | }, 76 | }, 77 | }) 78 | } 79 | 80 | var testAccMatrixContentConfig_plainText = ` 81 | resource "matrix_content" "foobar" { 82 | file_path = "%s" 83 | file_name = "%s" 84 | file_type = "%s" 85 | }` 86 | 87 | func TestAccMatrixContent_PlainTextUpload(t *testing.T) { 88 | upload := &testAccMatrixContentUpload{ 89 | FilePath: path.Join(testAccTestDataDir(), ".test_data/words.txt"), 90 | FileName: "hello.txt", 91 | FileType: "text/plain", 92 | } 93 | conf := fmt.Sprintf(testAccMatrixContentConfig_plainText, upload.FilePath, upload.FileName, upload.FileType) 94 | 95 | resource.Test(t, resource.TestCase{ 96 | PreCheck: func() { testAccPreCheck(t) }, 97 | Providers: testAccProviders, 98 | // We don't check if content get destroyed because it isn't 99 | //CheckDestroy: testAccCheckMatrixContentDestroy, 100 | Steps: []resource.TestStep{ 101 | { 102 | Config: conf, 103 | Check: resource.ComposeTestCheckFunc( 104 | testAccCheckMatrixContentExists("matrix_content.foobar"), 105 | //testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 106 | testAccCheckMatrixContentMatchesFile("matrix_content.foobar", upload), 107 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 108 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 109 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 110 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 111 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_path", upload.FilePath), 112 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_name", upload.FileName), 113 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_type", upload.FileType), 114 | ), 115 | }, 116 | }, 117 | }) 118 | } 119 | 120 | var testAccMatrixContentConfig_binary = ` 121 | resource "matrix_content" "foobar" { 122 | file_path = "%s" 123 | file_name = "%s" 124 | file_type = "%s" 125 | }` 126 | 127 | func TestAccMatrixContent_BinaryUpload(t *testing.T) { 128 | upload := &testAccMatrixContentUpload{ 129 | FilePath: path.Join(testAccTestDataDir(), ".test_data/deadbeef.bin"), 130 | FileName: "beef.bin", 131 | FileType: "application/octet-stream", 132 | } 133 | conf := fmt.Sprintf(testAccMatrixContentConfig_plainText, upload.FilePath, upload.FileName, upload.FileType) 134 | 135 | resource.Test(t, resource.TestCase{ 136 | PreCheck: func() { testAccPreCheck(t) }, 137 | Providers: testAccProviders, 138 | // We don't check if content get destroyed because it isn't 139 | //CheckDestroy: testAccCheckMatrixContentDestroy, 140 | Steps: []resource.TestStep{ 141 | { 142 | Config: conf, 143 | Check: resource.ComposeTestCheckFunc( 144 | testAccCheckMatrixContentExists("matrix_content.foobar"), 145 | //testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 146 | testAccCheckMatrixContentMatchesFile("matrix_content.foobar", upload), 147 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 148 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 149 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 150 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 151 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_path", upload.FilePath), 152 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_name", upload.FileName), 153 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_type", upload.FileType), 154 | ), 155 | }, 156 | }, 157 | }) 158 | } 159 | 160 | var testAccMatrixContentConfig_uploadNoName = ` 161 | resource "matrix_content" "foobar" { 162 | file_path = "%s" 163 | file_type = "%s" 164 | }` 165 | 166 | func TestAccMatrixContent_UploadNoName(t *testing.T) { 167 | upload := &testAccMatrixContentUpload{ 168 | FilePath: path.Join(testAccTestDataDir(), ".test_data/deadbeef.bin"), 169 | FileName: "", // expected name 170 | FileType: "application/octet-stream", 171 | } 172 | conf := fmt.Sprintf(testAccMatrixContentConfig_uploadNoName, upload.FilePath, upload.FileType) 173 | 174 | resource.Test(t, resource.TestCase{ 175 | PreCheck: func() { testAccPreCheck(t) }, 176 | Providers: testAccProviders, 177 | // We don't check if content get destroyed because it isn't 178 | //CheckDestroy: testAccCheckMatrixContentDestroy, 179 | Steps: []resource.TestStep{ 180 | { 181 | Config: conf, 182 | Check: resource.ComposeTestCheckFunc( 183 | testAccCheckMatrixContentExists("matrix_content.foobar"), 184 | //testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 185 | testAccCheckMatrixContentMatchesFile("matrix_content.foobar", upload), 186 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 187 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 188 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 189 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 190 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_path", upload.FilePath), 191 | resource.TestCheckNoResourceAttr("matrix_content.foobar", "file_name"), 192 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_type", upload.FileType), 193 | ), 194 | }, 195 | }, 196 | }) 197 | } 198 | 199 | var testAccMatrixContentConfig_uploadNoType = ` 200 | resource "matrix_content" "foobar" { 201 | file_path = "%s" 202 | file_name = "%s" 203 | }` 204 | 205 | func TestAccMatrixContent_UploadNoType(t *testing.T) { 206 | upload := &testAccMatrixContentUpload{ 207 | FilePath: path.Join(testAccTestDataDir(), ".test_data/deadbeef.bin"), 208 | FileName: "beef.bin", 209 | FileType: "application/octet-stream", // expected type 210 | } 211 | conf := fmt.Sprintf(testAccMatrixContentConfig_uploadNoType, upload.FilePath, upload.FileName) 212 | 213 | resource.Test(t, resource.TestCase{ 214 | PreCheck: func() { testAccPreCheck(t) }, 215 | Providers: testAccProviders, 216 | // We don't check if content get destroyed because it isn't 217 | //CheckDestroy: testAccCheckMatrixContentDestroy, 218 | Steps: []resource.TestStep{ 219 | { 220 | Config: conf, 221 | Check: resource.ComposeTestCheckFunc( 222 | testAccCheckMatrixContentExists("matrix_content.foobar"), 223 | //testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 224 | testAccCheckMatrixContentMatchesFile("matrix_content.foobar", upload), 225 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 226 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 227 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 228 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 229 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_path", upload.FilePath), 230 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_name", upload.FileName), 231 | resource.TestCheckNoResourceAttr("matrix_content.foobar", "file_type"), 232 | ), 233 | }, 234 | }, 235 | }) 236 | } 237 | 238 | var testAccMatrixContentConfig_uploadNoNameOrType = ` 239 | resource "matrix_content" "foobar" { 240 | file_path = "%s" 241 | }` 242 | 243 | func TestAccMatrixContent_UploadNoNameOrType(t *testing.T) { 244 | upload := &testAccMatrixContentUpload{ 245 | FilePath: path.Join(testAccTestDataDir(), ".test_data/deadbeef.bin"), 246 | FileName: "", // expected name 247 | FileType: "application/octet-stream", // expected type 248 | } 249 | conf := fmt.Sprintf(testAccMatrixContentConfig_uploadNoNameOrType, upload.FilePath) 250 | 251 | resource.Test(t, resource.TestCase{ 252 | PreCheck: func() { testAccPreCheck(t) }, 253 | Providers: testAccProviders, 254 | // We don't check if content get destroyed because it isn't 255 | //CheckDestroy: testAccCheckMatrixContentDestroy, 256 | Steps: []resource.TestStep{ 257 | { 258 | Config: conf, 259 | Check: resource.ComposeTestCheckFunc( 260 | testAccCheckMatrixContentExists("matrix_content.foobar"), 261 | //testAccCheckMatrixContentMatchesUpload("matrix_content.foobar", upload), 262 | testAccCheckMatrixContentMatchesFile("matrix_content.foobar", upload), 263 | testAccCheckMatrixContentIdMatchesProperties("matrix_content.foobar"), 264 | resource.TestMatchResourceAttr("matrix_content.foobar", "id", regexp.MustCompile("^mxc://[a-zA-Z0-9.:\\-_]+/[a-zA-Z0-9]+")), 265 | resource.TestMatchResourceAttr("matrix_content.foobar", "origin", regexp.MustCompile("^[a-zA-Z0-9.:\\-_]+$")), 266 | resource.TestMatchResourceAttr("matrix_content.foobar", "media_id", regexp.MustCompile("^[a-zA-Z0-9]+$")), 267 | resource.TestCheckResourceAttr("matrix_content.foobar", "file_path", upload.FilePath), 268 | resource.TestCheckNoResourceAttr("matrix_content.foobar", "file_name"), 269 | resource.TestCheckNoResourceAttr("matrix_content.foobar", "file_type"), 270 | ), 271 | }, 272 | }, 273 | }) 274 | } 275 | 276 | func testAccCheckMatrixContentExists(n string) resource.TestCheckFunc { 277 | return func(s *terraform.State) error { 278 | meta := testAccProvider.Meta().(Metadata) 279 | rs, ok := s.RootModule().Resources[n] 280 | 281 | if !ok { 282 | return fmt.Errorf("not found: %s", n) 283 | } 284 | 285 | if rs.Primary.ID == "" { 286 | return fmt.Errorf("record id not set") 287 | } 288 | 289 | origin := rs.Primary.Attributes["origin"] 290 | mediaId := rs.Primary.Attributes["media_id"] 291 | 292 | stream, _, err := api.DownloadFile(meta.ClientApiUrl, origin, mediaId) 293 | if stream != nil { 294 | defer (*stream).Close() 295 | io.Copy(ioutil.Discard, *stream) 296 | } 297 | if err != nil { 298 | return err 299 | } 300 | 301 | return nil 302 | } 303 | } 304 | 305 | func testAccCheckMatrixContentMatchesFile(n string, upload *testAccMatrixContentUpload) resource.TestCheckFunc { 306 | return func(s *terraform.State) error { 307 | meta := testAccProvider.Meta().(Metadata) 308 | rs, ok := s.RootModule().Resources[n] 309 | 310 | if !ok { 311 | return fmt.Errorf("not found: %s", n) 312 | } 313 | 314 | if rs.Primary.ID == "" { 315 | return fmt.Errorf("record id not set") 316 | } 317 | 318 | origin := rs.Primary.Attributes["origin"] 319 | mediaId := rs.Primary.Attributes["media_id"] 320 | 321 | download, headers, err := api.DownloadFile(meta.ClientApiUrl, origin, mediaId) 322 | contents := make([]byte, 0) 323 | if download != nil { 324 | defer (*download).Close() 325 | contents, err = ioutil.ReadAll(*download) 326 | if err != nil { 327 | return err 328 | } 329 | } 330 | if err != nil { 331 | return err 332 | } 333 | 334 | // Ensure the Content is populated 335 | if upload.FilePath != "" { 336 | f, err := os.Open(upload.FilePath) 337 | if err != nil { 338 | return err 339 | } 340 | defer f.Close() 341 | 342 | b, err := ioutil.ReadAll(f) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | upload.Content = b 348 | } 349 | 350 | // Compare bytes 351 | if len(contents) != len(upload.Content) { 352 | return fmt.Errorf("content length mismatch. expected: %d got: %d", len(upload.Content), len(contents)) 353 | } 354 | for i := range contents { 355 | d := contents[i] 356 | e := upload.Content[i] 357 | if d != e { 358 | return fmt.Errorf("byte mismatch at index %d/%d. expected: %b got: %b", i, len(contents), e, d) 359 | } 360 | } 361 | 362 | // Compare content type 363 | contentType := headers.Get("Content-Type") 364 | if contentType != upload.FileType { 365 | return fmt.Errorf("content type mismatch. expected: %s got: %s", upload.FileType, contentType) 366 | } 367 | 368 | // Compare file name 369 | contentDisposition := headers.Get("content-disposition") 370 | _, params, err := mime.ParseMediaType(contentDisposition) 371 | if err != nil { 372 | if err.Error() != "mime: no media type" { 373 | return err 374 | } 375 | 376 | params = map[string]string{"filename": ""} 377 | } 378 | fileName := params["filename"] 379 | if fileName != upload.FileName { 380 | return fmt.Errorf("file name mismatch. expected: %s got: %s", upload.FileName, fileName) 381 | } 382 | 383 | return nil 384 | } 385 | } 386 | 387 | func testAccCheckMatrixContentMatchesUpload(n string, uploaded *testAccMatrixContentUpload) resource.TestCheckFunc { 388 | return func(s *terraform.State) error { 389 | //meta := testAccProvider.Meta().(Metadata) 390 | rs, ok := s.RootModule().Resources[n] 391 | 392 | if !ok { 393 | return fmt.Errorf("not found: %s", n) 394 | } 395 | 396 | if rs.Primary.ID == "" { 397 | return fmt.Errorf("record id not set") 398 | } 399 | 400 | mxc := rs.Primary.ID 401 | origin := rs.Primary.Attributes["origin"] 402 | mediaId := rs.Primary.Attributes["media_id"] 403 | 404 | if mxc != uploaded.Mxc { 405 | return fmt.Errorf("mxc does not match. expected: %s got: %s", uploaded.Mxc, mxc) 406 | } 407 | if origin != uploaded.Origin { 408 | return fmt.Errorf("origin does not match. expected: %s got: %s", uploaded.Origin, origin) 409 | } 410 | if mediaId != uploaded.MediaId { 411 | return fmt.Errorf("media_id does not match. expected: %s got: %s", uploaded.MediaId, mediaId) 412 | } 413 | 414 | return nil 415 | } 416 | } 417 | 418 | func testAccCheckMatrixContentIdMatchesProperties(n string) resource.TestCheckFunc { 419 | return func(s *terraform.State) error { 420 | rs, ok := s.RootModule().Resources[n] 421 | if !ok { 422 | return fmt.Errorf("not found: %s", n) 423 | } 424 | 425 | mxc := rs.Primary.ID 426 | origin := rs.Primary.Attributes["origin"] 427 | mediaId := rs.Primary.Attributes["media_id"] 428 | 429 | calcMxc := fmt.Sprintf("mxc://%s/%s", origin, mediaId) 430 | if calcMxc != mxc { 431 | return fmt.Errorf("id and calculated mxc are different. expected: %s got: %s", calcMxc, mxc) 432 | } 433 | 434 | return nil 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /matrix/resource_matrix_room.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/helper/schema" 5 | "fmt" 6 | "log" 7 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | func resourceRoom() *schema.Resource { 13 | return &schema.Resource{ 14 | Exists: resourceRoomExists, 15 | Create: resourceRoomCreate, 16 | Read: resourceRoomRead, 17 | Update: resourceRoomUpdate, 18 | Delete: resourceRoomDelete, 19 | 20 | Schema: map[string]*schema.Schema{ 21 | "creator_user_id": { 22 | Type: schema.TypeString, 23 | Optional: true, 24 | Computed: true, 25 | ForceNew: true, 26 | }, 27 | "member_access_token": { 28 | Type: schema.TypeString, 29 | Required: true, 30 | }, 31 | "room_id": { 32 | Type: schema.TypeString, 33 | Optional: true, 34 | Computed: true, 35 | ForceNew: true, 36 | }, 37 | "preset": { 38 | Type: schema.TypeString, 39 | Optional: true, 40 | ForceNew: true, 41 | // Ignored if no creator 42 | }, 43 | "name": { 44 | Type: schema.TypeString, 45 | Optional: true, 46 | Computed: true, 47 | }, 48 | "avatar_mxc": { 49 | Type: schema.TypeString, 50 | Optional: true, 51 | Computed: true, 52 | }, 53 | "topic": { 54 | Type: schema.TypeString, 55 | Optional: true, 56 | Computed: true, 57 | }, 58 | "invite_user_ids": { 59 | Type: schema.TypeSet, 60 | Elem: &schema.Schema{ 61 | Type: schema.TypeString, 62 | }, 63 | Optional: true, 64 | ForceNew: true, 65 | // Ignored if no creator 66 | }, 67 | "local_alias_localpart": { 68 | Type: schema.TypeString, 69 | Optional: true, 70 | ForceNew: true, 71 | // Ignored if no creator 72 | }, 73 | "guests_allowed": { 74 | Type: schema.TypeBool, 75 | Optional: true, 76 | Computed: true, 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | func resourceRoomCreate(d *schema.ResourceData, m interface{}) error { 83 | meta := m.(Metadata) 84 | 85 | creatorIdRaw := nilIfEmptyString(d.Get("creator_user_id")) 86 | memberAccessToken := d.Get("member_access_token").(string) 87 | roomIdRaw := nilIfEmptyString(d.Get("room_id")) 88 | 89 | presetRaw := d.Get("preset").(string) 90 | nameRaw := nilIfEmptyString(d.Get("name")) 91 | avatarMxcRaw := nilIfEmptyString(d.Get("avatar_mxc")) 92 | topicRaw := nilIfEmptyString(d.Get("topic")) 93 | aliasLocalpartRaw := d.Get("local_alias_localpart").(string) 94 | guestsAllowed := d.Get("guests_allowed").(bool) 95 | invitedUserIds := setOfStrings(d.Get("invite_user_ids").(*schema.Set)) 96 | 97 | hasCreator := creatorIdRaw != nil 98 | hasRoomId := roomIdRaw != nil 99 | 100 | if hasCreator && hasRoomId { 101 | return fmt.Errorf("cannot specify both a creator and room_id") 102 | } 103 | 104 | if !hasCreator && !hasRoomId { 105 | return fmt.Errorf("a creator or room_id must be specified") 106 | } 107 | 108 | if hasCreator { 109 | log.Println("[DEBUG] Room creator set, creating room") 110 | request := &api.CreateRoomRequest{ 111 | Preset: presetRaw, 112 | AliasLocalpart: aliasLocalpartRaw, 113 | InviteUserIds: invitedUserIds, 114 | } 115 | 116 | stateEvents := make([]api.CreateRoomStateEvent, 0) 117 | if nameRaw != nil { 118 | log.Println("[DEBUG] Including room name state event") 119 | stateEvents = append(stateEvents, api.CreateRoomStateEvent{ 120 | Type: "m.room.name", 121 | Content: api.RoomNameEventContent{Name: nameRaw.(string)}, 122 | }) 123 | } 124 | if avatarMxcRaw != nil { 125 | log.Println("[DEBUG] Including room avatar state event") 126 | stateEvents = append(stateEvents, api.CreateRoomStateEvent{ 127 | Type: "m.room.avatar", 128 | Content: api.RoomAvatarEventContent{AvatarMxc: avatarMxcRaw.(string)}, 129 | }) 130 | } 131 | if topicRaw != nil { 132 | log.Println("[DEBUG] Including room topic state event") 133 | stateEvents = append(stateEvents, api.CreateRoomStateEvent{ 134 | Type: "m.room.topic", 135 | Content: api.RoomTopicEventContent{Topic: topicRaw.(string)}, 136 | }) 137 | } 138 | if guestsAllowed { 139 | log.Println("[DEBUG] Including room guest access state event (can_join)") 140 | stateEvents = append(stateEvents, api.CreateRoomStateEvent{ 141 | Type: "m.room.guest_access", 142 | Content: api.RoomGuestAccessEventContent{Policy: "can_join"}, 143 | }) 144 | } else { 145 | log.Println("[DEBUG] Including room guest access state event (forbidden)") 146 | stateEvents = append(stateEvents, api.CreateRoomStateEvent{ 147 | Type: "m.room.guest_access", 148 | Content: api.RoomGuestAccessEventContent{Policy: "forbidden"}, 149 | }) 150 | } 151 | request.InitialState = stateEvents 152 | 153 | response := &api.RoomIdResponse{} 154 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/createRoom") 155 | log.Println("[DEBUG] Creating room:", urlStr) 156 | err := api.DoRequest("POST", urlStr, request, response, memberAccessToken) 157 | if err != nil { 158 | return fmt.Errorf("error creating room: %s", err) 159 | } 160 | 161 | d.SetId(response.RoomId) 162 | d.Set("room_id", response.RoomId) 163 | } else { 164 | d.SetId(roomIdRaw.(string)) 165 | } 166 | 167 | return resourceRoomRead(d, meta) 168 | } 169 | 170 | func resourceRoomExists(d *schema.ResourceData, m interface{}) (bool, error) { 171 | meta := m.(Metadata) 172 | 173 | memberAccessToken := d.Get("member_access_token").(string) 174 | roomIdRaw := nilIfEmptyString(d.Get("room_id")) 175 | 176 | if roomIdRaw == nil { 177 | log.Println("[DEBUG] Considering room deleted, no room ID") 178 | return false, nil 179 | } 180 | 181 | // First identify who the user is 182 | log.Println("[DEBUG] Doing whoami on:", d.Id()) 183 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/account/whoami") 184 | whoAmIResponse := &api.WhoAmIResponse{} 185 | err := api.DoRequest("GET", urlStr, nil, whoAmIResponse, memberAccessToken) 186 | if err != nil { 187 | // We say true so that Terraform won't accidentally delete the room 188 | return true, fmt.Errorf("error performing whoami: %s", err) 189 | } 190 | 191 | // Now that we have user's ID, let's make sure they are a member 192 | memberEventResponse := &api.RoomMemberEventContent{} 193 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms", roomIdRaw.(string), "/state/m.room.member/", whoAmIResponse.UserId) 194 | log.Println("[DEBUG] Ensuring user is in room:", urlStr) 195 | err = api.DoRequest("GET", urlStr, nil, memberEventResponse, memberAccessToken) 196 | if err != nil { 197 | // An error accessing the room means it doesn't exist anymore 198 | return false, fmt.Errorf("error getting member event for user: %s", err) 199 | } 200 | 201 | if memberEventResponse.Membership != "join" { 202 | return false, fmt.Errorf("member is not in the room") 203 | } 204 | 205 | return true, nil 206 | } 207 | 208 | func resourceRoomRead(d *schema.ResourceData, m interface{}) error { 209 | meta := m.(Metadata) 210 | 211 | memberAccessToken := d.Get("member_access_token").(string) 212 | roomIdRaw := nilIfEmptyString(d.Get("room_id")) 213 | 214 | if roomIdRaw == nil { 215 | return fmt.Errorf("no room_id") 216 | } 217 | 218 | nameResponse := &api.RoomNameEventContent{} 219 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomIdRaw.(string), "/state/m.room.name") 220 | log.Println("[DEBUG] Getting room name:", urlStr) 221 | err := api.DoRequest("GET", urlStr, nil, nameResponse, memberAccessToken) 222 | if err != nil { 223 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 224 | return fmt.Errorf("error getting room name: %s", err) 225 | } 226 | } 227 | 228 | avatarResponse := &api.RoomAvatarEventContent{} 229 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomIdRaw.(string), "/state/m.room.avatar") 230 | log.Println("[DEBUG] Getting room avatar:", urlStr) 231 | err = api.DoRequest("GET", urlStr, nil, avatarResponse, memberAccessToken) 232 | if err != nil { 233 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 234 | return fmt.Errorf("error getting room avatar: %s", err) 235 | } 236 | } 237 | 238 | topicResponse := &api.RoomTopicEventContent{} 239 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomIdRaw.(string), "/state/m.room.topic") 240 | log.Println("[DEBUG] Getting room topic:", urlStr) 241 | err = api.DoRequest("GET", urlStr, nil, topicResponse, memberAccessToken) 242 | if err != nil { 243 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 244 | return fmt.Errorf("error getting room topic: %s", err) 245 | } 246 | } 247 | 248 | guestResponse := &api.RoomGuestAccessEventContent{} 249 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomIdRaw.(string), "/state/m.room.guest_access") 250 | log.Println("[DEBUG] Getting room guest access:", urlStr) 251 | err = api.DoRequest("GET", urlStr, nil, guestResponse, memberAccessToken) 252 | if err != nil { 253 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 254 | return fmt.Errorf("error getting room guest access policy: %s", err) 255 | } 256 | } 257 | 258 | creatorResponse := &api.RoomCreateEventContent{} 259 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomIdRaw.(string), "/state/m.room.create") 260 | log.Println("[DEBUG] Getting room create event:", urlStr) 261 | err = api.DoRequest("GET", urlStr, nil, creatorResponse, memberAccessToken) 262 | if err != nil { 263 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 264 | return fmt.Errorf("error getting room creator: %s", err) 265 | } 266 | } 267 | 268 | d.Set("name", nameResponse.Name) 269 | d.Set("avatar_mxc", avatarResponse.AvatarMxc) 270 | d.Set("topic", topicResponse.Topic) 271 | d.Set("creator_user_id", creatorResponse.CreatorUserId) 272 | 273 | if guestResponse.Policy == "can_join" { 274 | d.Set("guests_allowed", true) 275 | } else { 276 | d.Set("guests_allowed", false) 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func resourceRoomUpdate(d *schema.ResourceData, m interface{}) error { 283 | meta := m.(Metadata) 284 | 285 | memberAccessToken := d.Get("member_access_token").(string) 286 | roomIdRaw := nilIfEmptyString(d.Get("room_id")) 287 | 288 | if roomIdRaw == nil { 289 | return fmt.Errorf("no room_id") 290 | } 291 | 292 | if d.HasChange("name") { 293 | request := &api.RoomNameEventContent{Name: d.Get("name").(string)} 294 | response := &api.EventIdResponse{} 295 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms", roomIdRaw.(string), "/state/m.room.name") 296 | log.Println("[DEBUG] Updating room name:", urlStr) 297 | err := api.DoRequest("PUT", urlStr, request, response, memberAccessToken) 298 | if err != nil { 299 | return err 300 | } 301 | } 302 | 303 | if d.HasChange("avatar_mxc") { 304 | request := &api.RoomAvatarEventContent{AvatarMxc: d.Get("avatar_mxc").(string)} 305 | response := &api.EventIdResponse{} 306 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms", roomIdRaw.(string), "/state/m.room.avatar") 307 | log.Println("[DEBUG] Updating room avatar:", urlStr) 308 | err := api.DoRequest("PUT", urlStr, request, response, memberAccessToken) 309 | if err != nil { 310 | return err 311 | } 312 | } 313 | 314 | if d.HasChange("topic") { 315 | request := &api.RoomTopicEventContent{Topic: d.Get("topic").(string)} 316 | response := &api.EventIdResponse{} 317 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms", roomIdRaw.(string), "/state/m.room.topic") 318 | log.Println("[DEBUG] Updating room topic:", urlStr) 319 | err := api.DoRequest("PUT", urlStr, request, response, memberAccessToken) 320 | if err != nil { 321 | return err 322 | } 323 | } 324 | 325 | if d.HasChange("guests_allowed") { 326 | policy := "forbidden" 327 | if d.Get("guests_allowed").(bool) { 328 | policy = "can_join" 329 | } 330 | request := &api.RoomGuestAccessEventContent{Policy: policy} 331 | response := &api.EventIdResponse{} 332 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms", roomIdRaw.(string), "/state/m.room.guest_access") 333 | log.Println("[DEBUG] Updating room guest access policy:", urlStr) 334 | err := api.DoRequest("PUT", urlStr, request, response, memberAccessToken) 335 | if err != nil { 336 | return err 337 | } 338 | } 339 | 340 | return nil 341 | } 342 | 343 | func resourceRoomDelete(d *schema.ResourceData, m interface{}) error { 344 | meta := m.(Metadata) 345 | 346 | memberAccessToken := d.Get("member_access_token").(string) 347 | roomId := nilIfEmptyString(d.Get("room_id")).(string) 348 | 349 | log.Println("[DEBUG] Performing whoami on member access token") 350 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/account/whoami") 351 | whoAmIResponse := &api.WhoAmIResponse{} 352 | err := api.DoRequest("GET", urlStr, nil, whoAmIResponse, memberAccessToken) 353 | if err != nil { 354 | return fmt.Errorf("error performing whoami: %s", err) 355 | } 356 | hsDomain, err := getDomainName(whoAmIResponse.UserId) 357 | if err != nil { 358 | return fmt.Errorf("error parsing user id: %s", err) 359 | } 360 | 361 | // Rooms cannot technically be deleted, so we just abandon them instead 362 | // Abandoning means kicking everyone and leaving it to rot away. Before we leave though, we'll make sure no one can 363 | // get back in. 364 | 365 | // First step: remove all local aliases (by fetching them first, then deleting them) 366 | aliasesResponse := &api.RoomAliasesEventContent{} 367 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.aliases/", hsDomain) 368 | log.Println("[DEBUG] Getting room aliases:", urlStr) 369 | err = api.DoRequest("GET", urlStr, nil, aliasesResponse, memberAccessToken) 370 | if err != nil { 371 | if mtxErr, ok := err.(*api.ErrorResponse); !ok || mtxErr.ErrorCode != api.ErrCodeNotFound { 372 | return fmt.Errorf("error getting room aliases: %s", err) 373 | } 374 | 375 | // We got a 404 on the event, so we'll just fake it and say we got no aliases 376 | aliasesResponse.Aliases = make([]string, 0) 377 | } 378 | for _, alias := range aliasesResponse.Aliases { 379 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/directory/room/", url.QueryEscape(alias)) 380 | log.Println("[DEBUG] Deleting room alias:", urlStr) 381 | err = api.DoRequest("DELETE", urlStr, nil, nil, memberAccessToken) 382 | if err != nil { 383 | return fmt.Errorf("failed to delete alias %s: %s", alias, err) 384 | } 385 | } 386 | 387 | // Set the room to invite only 388 | joinRulesRequest := &api.RoomJoinRulesEventContent{Policy: "invite"} 389 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.join_rules") 390 | log.Println("[DEBUG] Setting join rules:", urlStr) 391 | err = api.DoRequest("PUT", urlStr, joinRulesRequest, nil, memberAccessToken) 392 | if err != nil { 393 | return fmt.Errorf("error setting join rules to invite only: %s", err) 394 | } 395 | 396 | // Disable guest access 397 | guestAccessRequest := &api.RoomGuestAccessEventContent{Policy: "forbidden"} 398 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.guest_access") 399 | log.Println("[DEBUG] Disabling guest access:", urlStr) 400 | err = api.DoRequest("PUT", urlStr, guestAccessRequest, nil, memberAccessToken) 401 | if err != nil { 402 | return fmt.Errorf("error disabling guest access: %s", err) 403 | } 404 | 405 | // Kick everyone 406 | membersResponse := &api.RoomMembersResponse{} 407 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/members") 408 | log.Println("[DEBUG] Getting room members:", urlStr) 409 | err = api.DoRequest("GET", urlStr, nil, membersResponse, memberAccessToken) 410 | if err != nil { 411 | return fmt.Errorf("error getting membership list: %s", err) 412 | } 413 | for _, member := range membersResponse.Chunk { 414 | if member.Content == nil { 415 | return fmt.Errorf("member %s has no content in their member event", member.StateKey) 416 | } 417 | 418 | if member.StateKey == whoAmIResponse.UserId { 419 | // We don't to kick ourselves yet 420 | continue 421 | } 422 | 423 | if member.Content.Membership == "invite" || member.Content.Membership == "join" { 424 | kickRequest := &api.KickRequest{ 425 | UserId: member.StateKey, 426 | Reason: "This room is being deleted in Terraform", 427 | } 428 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/kick") 429 | log.Println("[DEBUG] Kicking", kickRequest.UserId, ": ", urlStr) 430 | err = api.DoRequest("POST", urlStr, kickRequest, nil, memberAccessToken) 431 | if err != nil { 432 | return fmt.Errorf("error kicking %s: %s", member.StateKey, err) 433 | } 434 | } 435 | } 436 | 437 | // Leave (forget) the room 438 | // The spec says we should be able to forget and have that leave us, however this isn't what synapse 439 | // does in practice: https://github.com/matrix-org/matrix-doc/issues/1011 440 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/leave") 441 | log.Println("[DEBUG] Leaving room:", urlStr) 442 | err = api.DoRequest("POST", urlStr, nil, nil, memberAccessToken) 443 | if err != nil { 444 | return fmt.Errorf("error leaving the room: %s", err) 445 | } 446 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/forget") 447 | log.Println("[DEBUG] Forgetting room:", urlStr) 448 | err = api.DoRequest("POST", urlStr, nil, nil, memberAccessToken) 449 | if err != nil { 450 | return fmt.Errorf("error forgetting the room: %s", err) 451 | } 452 | 453 | // Note: We can't do anything about the room's history, so we leave that untouched. 454 | return nil 455 | } 456 | -------------------------------------------------------------------------------- /matrix/resource_matrix_room_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | "github.com/hashicorp/terraform/helper/resource" 6 | "fmt" 7 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 8 | "github.com/hashicorp/terraform/terraform" 9 | "strconv" 10 | "regexp" 11 | "net/http" 12 | "net/url" 13 | ) 14 | 15 | type testAccMatrixRoom struct { 16 | RoomId string 17 | CreatorUserId string 18 | Name string 19 | AvatarMxc string 20 | Topic string 21 | GuestsAllowed bool 22 | CreatorToken string 23 | Preset string 24 | } 25 | 26 | func testAccCreateMatrixRoom(name string, avatarMxc string, topic string, guestsAllowed bool, preset string) (*testAccMatrixRoom) { 27 | guestAccess := api.RoomGuestAccessEventContent{Policy: "forbidden"} 28 | if guestsAllowed { 29 | guestAccess.Policy = "can_join" 30 | } 31 | 32 | request := &api.CreateRoomRequest{ 33 | Preset: preset, 34 | InitialState: []api.CreateRoomStateEvent{ 35 | {Type: "m.room.name", Content: api.RoomNameEventContent{Name: name}}, 36 | {Type: "m.room.avatar", Content: api.RoomAvatarEventContent{AvatarMxc: avatarMxc}}, 37 | {Type: "m.room.topic", Content: api.RoomTopicEventContent{Topic: topic}}, 38 | {Type: "m.room.guest_access", Content: guestAccess}, 39 | }, 40 | } 41 | 42 | response := &api.RoomIdResponse{} 43 | urlStr := api.MakeUrl(testAccClientServerUrl(), "/_matrix/client/r0/createRoom") 44 | err := api.DoRequest("POST", urlStr, request, response, testAccAdminToken()) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | creatorResponse := &api.RoomCreateEventContent{} 50 | urlStr = api.MakeUrl(testAccClientServerUrl(), "/_matrix/client/r0/rooms", response.RoomId, "/state/m.room.create") 51 | err = api.DoRequest("GET", urlStr, nil, creatorResponse, testAccAdminToken()) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | return &testAccMatrixRoom{ 57 | RoomId: response.RoomId, 58 | CreatorUserId: creatorResponse.CreatorUserId, 59 | Name: name, 60 | AvatarMxc: avatarMxc, 61 | Topic: topic, 62 | GuestsAllowed: guestsAllowed, 63 | CreatorToken: testAccAdminToken(), 64 | Preset: preset, 65 | } 66 | } 67 | 68 | var testAccMatrixRoomConfig_existingRoom = ` 69 | resource "matrix_room" "foobar" { 70 | room_id = "%s" 71 | member_access_token = "%s" 72 | }` 73 | 74 | func TestAccMatrixRoom_ExistingRoom(t *testing.T) { 75 | room := testAccCreateMatrixRoom("Sample", "mxc://localhost/AvatarHere", "This is a topic", true, "private_chat") 76 | conf := fmt.Sprintf(testAccMatrixRoomConfig_existingRoom, room.RoomId, room.CreatorToken) 77 | 78 | resource.Test(t, resource.TestCase{ 79 | PreCheck: func() { testAccPreCheck(t) }, 80 | Providers: testAccProviders, 81 | CheckDestroy: testAccCheckMatrixRoomDestroy, 82 | Steps: []resource.TestStep{ 83 | { 84 | Config: conf, 85 | Check: resource.ComposeTestCheckFunc( 86 | testAccCheckMatrixRoomExists("matrix_room.foobar"), 87 | testAccCheckMatrixRoomIdMatchesRoomId("matrix_room.foobar"), 88 | testAccCheckMatrixRoomMatchesCreated("matrix_room.foobar", room), 89 | resource.TestCheckResourceAttr("matrix_room.foobar", "id", room.RoomId), 90 | resource.TestCheckResourceAttr("matrix_room.foobar", "creator_user_id", room.CreatorUserId), 91 | resource.TestCheckResourceAttr("matrix_room.foobar", "member_access_token", room.CreatorToken), 92 | resource.TestCheckResourceAttr("matrix_room.foobar", "room_id", room.RoomId), 93 | resource.TestCheckNoResourceAttr("matrix_room.foobar", "preset"), 94 | resource.TestCheckResourceAttr("matrix_room.foobar", "name", room.Name), 95 | resource.TestCheckResourceAttr("matrix_room.foobar", "avatar_mxc", room.AvatarMxc), 96 | resource.TestCheckResourceAttr("matrix_room.foobar", "topic", room.Topic), 97 | resource.TestCheckNoResourceAttr("matrix_room.foobar", "invite_user_ids"), 98 | resource.TestCheckNoResourceAttr("matrix_room.foobar", "local_alias_localpart"), 99 | resource.TestCheckResourceAttr("matrix_room.foobar", "guests_allowed", strconv.FormatBool(room.GuestsAllowed)), 100 | ), 101 | }, 102 | }, 103 | }) 104 | } 105 | 106 | var testAccMatrixRoomConfig_newRoom = ` 107 | resource "matrix_room" "foobar" { 108 | creator_user_id = "%s" 109 | member_access_token = "%s" 110 | name = "%s" 111 | avatar_mxc = "%s" 112 | topic = "%s" 113 | guests_allowed = %t 114 | preset = "%s" 115 | }` 116 | 117 | func TestAccMatrixRoom_NewRoom(t *testing.T) { 118 | creator := testAccCreateTestUser("test_acc_room_create_new") 119 | room := &testAccMatrixRoom{ 120 | CreatorToken: creator.AccessToken, 121 | CreatorUserId: creator.UserId, 122 | GuestsAllowed: true, 123 | Topic: "This is a topic", 124 | AvatarMxc: "mxc://localhost/AvatarHere", 125 | Name: "Sample", 126 | Preset: "private_chat", 127 | } 128 | conf := fmt.Sprintf(testAccMatrixRoomConfig_newRoom, room.CreatorUserId, room.CreatorToken, room.Name, room.AvatarMxc, room.Topic, room.GuestsAllowed, room.Preset) 129 | 130 | resource.Test(t, resource.TestCase{ 131 | PreCheck: func() { testAccPreCheck(t) }, 132 | Providers: testAccProviders, 133 | CheckDestroy: testAccCheckMatrixRoomDestroy, 134 | Steps: []resource.TestStep{ 135 | { 136 | Config: conf, 137 | Check: resource.ComposeTestCheckFunc( 138 | testAccCheckMatrixRoomExists("matrix_room.foobar"), 139 | testAccCheckMatrixRoomIdMatchesRoomId("matrix_room.foobar"), 140 | resource.TestMatchResourceAttr("matrix_room.foobar", "id", regexp.MustCompile("^!.+")), 141 | resource.TestCheckResourceAttr("matrix_room.foobar", "creator_user_id", room.CreatorUserId), 142 | resource.TestCheckResourceAttr("matrix_room.foobar", "member_access_token", room.CreatorToken), 143 | resource.TestCheckResourceAttr("matrix_room.foobar", "preset", room.Preset), 144 | resource.TestCheckResourceAttr("matrix_room.foobar", "name", room.Name), 145 | resource.TestCheckResourceAttr("matrix_room.foobar", "avatar_mxc", room.AvatarMxc), 146 | resource.TestCheckResourceAttr("matrix_room.foobar", "topic", room.Topic), 147 | resource.TestCheckNoResourceAttr("matrix_room.foobar", "invite_user_ids"), 148 | resource.TestCheckNoResourceAttr("matrix_room.foobar", "local_alias_localpart"), 149 | resource.TestCheckResourceAttr("matrix_room.foobar", "guests_allowed", strconv.FormatBool(room.GuestsAllowed)), 150 | ), 151 | }, 152 | }, 153 | }) 154 | } 155 | 156 | var testAccMatrixRoomConfig_invites = ` 157 | resource "matrix_room" "foobar" { 158 | creator_user_id = "%s" 159 | member_access_token = "%s" 160 | invite_user_ids = ["%s", "%s"] 161 | }` 162 | 163 | func TestAccMatrixRoom_Invites(t *testing.T) { 164 | creator := testAccCreateTestUser("test_acc_room_invites") 165 | targetA := testAccCreateTestUser("test_acc_room_invites_user_a") 166 | targetB := testAccCreateTestUser("test_acc_room_invites_user_b") 167 | inviteUserIds := []string{targetA.UserId, targetB.UserId} 168 | room := &testAccMatrixRoom{ 169 | CreatorToken: creator.AccessToken, 170 | CreatorUserId: creator.UserId, 171 | GuestsAllowed: true, 172 | Topic: "This is a topic", 173 | AvatarMxc: "mxc://localhost/AvatarHere", 174 | Name: "Sample", 175 | Preset: "private_chat", 176 | } 177 | conf := fmt.Sprintf(testAccMatrixRoomConfig_invites, room.CreatorUserId, room.CreatorToken, inviteUserIds[0], inviteUserIds[1]) 178 | 179 | resource.Test(t, resource.TestCase{ 180 | PreCheck: func() { testAccPreCheck(t) }, 181 | Providers: testAccProviders, 182 | CheckDestroy: testAccCheckMatrixRoomDestroy, 183 | Steps: []resource.TestStep{ 184 | { 185 | Config: conf, 186 | Check: resource.ComposeTestCheckFunc( 187 | testAccCheckMatrixRoomExists("matrix_room.foobar"), 188 | testAccCheckMatrixRoomIdMatchesRoomId("matrix_room.foobar"), 189 | testAccCheckMatrixRoomInvitedUsers("matrix_room.foobar", inviteUserIds), 190 | resource.TestMatchResourceAttr("matrix_room.foobar", "id", regexp.MustCompile("^!.+")), 191 | resource.TestCheckResourceAttr("matrix_room.foobar", "creator_user_id", room.CreatorUserId), 192 | resource.TestCheckResourceAttr("matrix_room.foobar", "member_access_token", room.CreatorToken), 193 | ), 194 | }, 195 | }, 196 | }) 197 | } 198 | 199 | var testAccMatrixRoomConfig_localAlias = ` 200 | resource "matrix_room" "foobar" { 201 | creator_user_id = "%s" 202 | member_access_token = "%s" 203 | local_alias_localpart = "%s" 204 | }` 205 | 206 | func TestAccMatrixRoom_LocalAlias(t *testing.T) { 207 | creator := testAccCreateTestUser("test_acc_room_local_alias") 208 | room := &testAccMatrixRoom{ 209 | CreatorToken: creator.AccessToken, 210 | CreatorUserId: creator.UserId, 211 | GuestsAllowed: true, 212 | Topic: "This is a topic", 213 | AvatarMxc: "mxc://localhost/AvatarHere", 214 | Name: "Sample", 215 | Preset: "private_chat", 216 | } 217 | expectedAlias := "test_acc_room_local_alias" 218 | conf := fmt.Sprintf(testAccMatrixRoomConfig_localAlias, room.CreatorUserId, room.CreatorToken, expectedAlias) 219 | 220 | resource.Test(t, resource.TestCase{ 221 | PreCheck: func() { testAccPreCheck(t) }, 222 | Providers: testAccProviders, 223 | CheckDestroy: testAccCheckMatrixRoomDestroy, 224 | Steps: []resource.TestStep{ 225 | { 226 | Config: conf, 227 | Check: resource.ComposeTestCheckFunc( 228 | testAccCheckMatrixRoomExists("matrix_room.foobar"), 229 | testAccCheckMatrixRoomIdMatchesRoomId("matrix_room.foobar"), 230 | testAccCheckMatrixRoomLocalAliasMatches("matrix_room.foobar", expectedAlias, room.CreatorUserId), 231 | resource.TestMatchResourceAttr("matrix_room.foobar", "id", regexp.MustCompile("^!.+")), 232 | resource.TestCheckResourceAttr("matrix_room.foobar", "creator_user_id", room.CreatorUserId), 233 | resource.TestCheckResourceAttr("matrix_room.foobar", "member_access_token", room.CreatorToken), 234 | resource.TestCheckResourceAttr("matrix_room.foobar", "local_alias_localpart", expectedAlias), 235 | ), 236 | }, 237 | }, 238 | }) 239 | } 240 | 241 | func testAccCheckMatrixRoomDestroy(s *terraform.State) error { 242 | meta := testAccProvider.Meta().(Metadata) 243 | for _, rs := range s.RootModule().Resources { 244 | if rs.Type != "matrix_room" { 245 | continue 246 | } 247 | 248 | // We'll try joining the room to ensure we can't get in. We won't be able to verify a lot of the state events, 249 | // however not being able to get in is a good indicator that the room is abandoned. 250 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", rs.Primary.ID, "/join") 251 | err := api.DoRequest("POST", urlStr, nil, nil, rs.Primary.Attributes["member_access_token"]) 252 | if err == nil { 253 | return fmt.Errorf("lack of error when deleting room") 254 | } else { 255 | // HACK: We rely on the string error code when we should have an error code. 256 | // https://github.com/matrix-org/matrix-doc/issues/1338 257 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound || r.Message != "No known servers" { 258 | return fmt.Errorf("error ensuring room is not joinable: %s", err) 259 | } 260 | } 261 | } 262 | 263 | return nil 264 | } 265 | 266 | func testAccCheckMatrixRoomExists(n string) resource.TestCheckFunc { 267 | return func(s *terraform.State) error { 268 | meta := testAccProvider.Meta().(Metadata) 269 | rs, ok := s.RootModule().Resources[n] 270 | 271 | if !ok { 272 | return fmt.Errorf("not found: %s", n) 273 | } 274 | 275 | if rs.Primary.ID == "" { 276 | return fmt.Errorf("record id not set") 277 | } 278 | 279 | memberToken := rs.Primary.Attributes["member_access_token"] 280 | 281 | // We'll try to query something like the create event to prove the room exists 282 | response := &api.RoomCreateEventContent{} 283 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", rs.Primary.ID, "/state/m.room.create") 284 | err := api.DoRequest("GET", urlStr, nil, response, memberToken) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | if response.CreatorUserId == "" { 290 | return fmt.Errorf("creator user id is empty") 291 | } 292 | 293 | return nil 294 | } 295 | } 296 | 297 | func testAccCheckMatrixRoomMatchesCreated(n string, room *testAccMatrixRoom) resource.TestCheckFunc { 298 | return func(s *terraform.State) error { 299 | meta := testAccProvider.Meta().(Metadata) 300 | rs, ok := s.RootModule().Resources[n] 301 | 302 | if !ok { 303 | return fmt.Errorf("not found: %s", n) 304 | } 305 | 306 | if rs.Primary.ID == "" { 307 | return fmt.Errorf("record id not set") 308 | } 309 | 310 | memberAccessToken := rs.Primary.Attributes["member_access_token"] 311 | roomId := rs.Primary.ID 312 | 313 | nameResponse := &api.RoomNameEventContent{} 314 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.name") 315 | err := api.DoRequest("GET", urlStr, nil, nameResponse, memberAccessToken) 316 | if err != nil { 317 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 318 | return fmt.Errorf("error getting room name: %s", err) 319 | } 320 | } 321 | 322 | avatarResponse := &api.RoomAvatarEventContent{} 323 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.avatar") 324 | err = api.DoRequest("GET", urlStr, nil, avatarResponse, memberAccessToken) 325 | if err != nil { 326 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 327 | return fmt.Errorf("error getting room avatar: %s", err) 328 | } 329 | } 330 | 331 | topicResponse := &api.RoomTopicEventContent{} 332 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.topic") 333 | err = api.DoRequest("GET", urlStr, nil, topicResponse, memberAccessToken) 334 | if err != nil { 335 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 336 | return fmt.Errorf("error getting room topic: %s", err) 337 | } 338 | } 339 | 340 | guestResponse := &api.RoomGuestAccessEventContent{} 341 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.guest_access") 342 | err = api.DoRequest("GET", urlStr, nil, guestResponse, memberAccessToken) 343 | if err != nil { 344 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 345 | return fmt.Errorf("error getting room guest access policy: %s", err) 346 | } 347 | } 348 | 349 | creatorResponse := &api.RoomCreateEventContent{} 350 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.create") 351 | err = api.DoRequest("GET", urlStr, nil, creatorResponse, memberAccessToken) 352 | if err != nil { 353 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 354 | return fmt.Errorf("error getting room creator: %s", err) 355 | } 356 | } 357 | 358 | joinRulesResponse := &api.RoomJoinRulesEventContent{} 359 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.join_rules") 360 | err = api.DoRequest("GET", urlStr, nil, joinRulesResponse, memberAccessToken) 361 | if err != nil { 362 | if r, ok := err.(*api.ErrorResponse); !ok || r.StatusCode != http.StatusNotFound { 363 | return fmt.Errorf("error getting room join rule policy: %s", err) 364 | } 365 | } 366 | 367 | if creatorResponse.CreatorUserId != room.CreatorUserId { 368 | return fmt.Errorf("creator mismatch. expected: %s got: %s", room.CreatorUserId, creatorResponse.CreatorUserId) 369 | } 370 | if nameResponse.Name != room.Name { 371 | return fmt.Errorf("name mismatch. expected: %s got: %s", room.Name, nameResponse.Name) 372 | } 373 | if avatarResponse.AvatarMxc != room.AvatarMxc { 374 | return fmt.Errorf("avatar mismatch. expected: %s got: %s", room.AvatarMxc, avatarResponse.AvatarMxc) 375 | } 376 | if topicResponse.Topic != room.Topic { 377 | return fmt.Errorf("topic mismatch. expected: %s got: %s", room.Topic, topicResponse.Topic) 378 | } 379 | 380 | guestPolicy := "forbidden" 381 | if room.GuestsAllowed { 382 | guestPolicy = "can_join" 383 | } 384 | if guestResponse.Policy != guestPolicy { 385 | return fmt.Errorf("guest_access mismatch. expected: %s got: %s", guestPolicy, guestResponse.Policy) 386 | } 387 | 388 | joinPolicy := "public" 389 | if room.Preset == "private_chat" { 390 | joinPolicy = "invite" 391 | } 392 | if joinRulesResponse.Policy != joinPolicy { 393 | return fmt.Errorf("join_rules mismatch. expected: %s got: %s", joinPolicy, joinRulesResponse.Policy) 394 | } 395 | 396 | return nil 397 | } 398 | } 399 | 400 | func testAccCheckMatrixRoomIdMatchesRoomId(n string) resource.TestCheckFunc { 401 | return func(s *terraform.State) error { 402 | rs, ok := s.RootModule().Resources[n] 403 | 404 | if !ok { 405 | return fmt.Errorf("not found: %s", n) 406 | } 407 | 408 | if rs.Primary.ID == "" { 409 | return fmt.Errorf("record id not set") 410 | } 411 | 412 | if rs.Primary.ID != rs.Primary.Attributes["room_id"] { 413 | return fmt.Errorf("room_id and record id do not match") 414 | } 415 | 416 | return nil 417 | } 418 | } 419 | 420 | func testAccCheckMatrixRoomInvitedUsers(n string, invitedUserIds []string) resource.TestCheckFunc { 421 | return func(s *terraform.State) error { 422 | meta := testAccProvider.Meta().(Metadata) 423 | rs, ok := s.RootModule().Resources[n] 424 | 425 | if !ok { 426 | return fmt.Errorf("not found: %s", n) 427 | } 428 | 429 | if rs.Primary.ID == "" { 430 | return fmt.Errorf("record id not set") 431 | } 432 | 433 | memberAccessToken := rs.Primary.Attributes["member_access_token"] 434 | roomId := rs.Primary.ID 435 | 436 | for _, invitedUserId := range invitedUserIds { 437 | response := &api.RoomMemberEventContent{} 438 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/rooms/", roomId, "/state/m.room.member/", invitedUserId) 439 | err := api.DoRequest("GET", urlStr, nil, response, memberAccessToken) 440 | if err != nil { 441 | return fmt.Errorf("error getting room member %s: %s", invitedUserId, err) 442 | } 443 | if response.Membership != "invite" { 444 | return fmt.Errorf("user %s is not invited", invitedUserId) 445 | } 446 | } 447 | 448 | return nil 449 | } 450 | } 451 | 452 | func testAccCheckMatrixRoomLocalAliasMatches(n string, aliasLocalpart string, creatorUserId string) resource.TestCheckFunc { 453 | return func(s *terraform.State) error { 454 | meta := testAccProvider.Meta().(Metadata) 455 | rs, ok := s.RootModule().Resources[n] 456 | 457 | if !ok { 458 | return fmt.Errorf("not found: %s", n) 459 | } 460 | 461 | if rs.Primary.ID == "" { 462 | return fmt.Errorf("record id not set") 463 | } 464 | 465 | memberAccessToken := rs.Primary.Attributes["member_access_token"] 466 | roomId := rs.Primary.ID 467 | 468 | // We're forced to do an estimation on what the full alias will look like, so we try and get the 469 | // homeserver domain from the creator's user ID. This is bad practice, however most of the tests 470 | // will be run on localhost anyways, making this check not so bad. 471 | hsDomain, err := getDomainName(creatorUserId) 472 | if err != nil { 473 | return fmt.Errorf("error parsing creator user id: %s", err) 474 | } 475 | fullAlias := fmt.Sprintf("#%s:%s", aliasLocalpart, hsDomain) 476 | safeAlias := url.QueryEscape(fullAlias) 477 | 478 | response := &api.RoomDirectoryLookupResponse{} 479 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/directory/room/", safeAlias) 480 | err = api.DoRequest("GET", urlStr, nil, response, memberAccessToken) 481 | if err != nil { 482 | return fmt.Errorf("error querying alias: %s", err) 483 | } 484 | if response.RoomId != roomId { 485 | return fmt.Errorf("room id mismatch. expected: %s got: %s", roomId, response.RoomId) 486 | } 487 | 488 | return nil 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /matrix/resource_matrix_user.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/helper/schema" 5 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 6 | "log" 7 | "fmt" 8 | ) 9 | 10 | func resourceUser() *schema.Resource { 11 | return &schema.Resource{ 12 | Exists: resourceUserExists, 13 | Create: resourceUserCreate, 14 | Read: resourceUserRead, 15 | Update: resourceUserUpdate, 16 | Delete: resourceUserDelete, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "username": { 20 | Type: schema.TypeString, 21 | Optional: true, 22 | ForceNew: true, 23 | }, 24 | "password": { 25 | Type: schema.TypeString, 26 | Optional: true, 27 | ForceNew: true, // The api is just way too complicated for us to implement 28 | }, 29 | "access_token": { 30 | Type: schema.TypeString, 31 | Computed: true, 32 | Optional: true, 33 | ForceNew: true, 34 | }, 35 | "display_name": { 36 | Type: schema.TypeString, 37 | Computed: true, 38 | Optional: true, 39 | }, 40 | "avatar_mxc": { 41 | Type: schema.TypeString, 42 | Computed: true, 43 | Optional: true, 44 | }, 45 | }, 46 | } 47 | } 48 | 49 | func resourceUserCreate(d *schema.ResourceData, m interface{}) error { 50 | meta := m.(Metadata) 51 | 52 | usernameRaw := nilIfEmptyString(d.Get("username")) 53 | passwordRaw := nilIfEmptyString(d.Get("password")) 54 | accessTokenRaw := nilIfEmptyString(d.Get("access_token")) 55 | displayNameRaw := nilIfEmptyString(d.Get("display_name")) 56 | avatarMxcRaw := nilIfEmptyString(d.Get("avatar_mxc")) 57 | 58 | if passwordRaw == nil && accessTokenRaw == nil { 59 | return fmt.Errorf("either password or access_token must be supplied") 60 | } 61 | if passwordRaw != nil && accessTokenRaw != nil { 62 | return fmt.Errorf("both password and access_token cannot be supplied") 63 | } 64 | if passwordRaw != nil && usernameRaw == nil { 65 | return fmt.Errorf("username and password must be supplied") 66 | } 67 | 68 | if passwordRaw != nil { 69 | log.Println("[DEBUG] User register:", usernameRaw.(string)) 70 | response, err := api.DoRegister(meta.ClientApiUrl, usernameRaw.(string), passwordRaw.(string), "user") 71 | if err != nil { 72 | if r, ok := err.(*api.ErrorResponse); ok && r.ErrorCode == api.ErrCodeUserInUse { 73 | request := &api.LoginRequest{ 74 | Type: api.LoginTypePassword, 75 | Username: usernameRaw.(string), 76 | Password: passwordRaw.(string), 77 | } 78 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/login") 79 | log.Println("[DEBUG] Logging in:", usernameRaw.(string)) 80 | response := &api.LoginResponse{} 81 | err2 := api.DoRequest("POST", urlStr, request, response, "") 82 | if err2 != nil { 83 | return fmt.Errorf("error logging in as user: %s", err) 84 | } 85 | 86 | d.SetId(response.UserId) 87 | d.Set("access_token", response.AccessToken) 88 | } else { 89 | return fmt.Errorf("error creating user: %s", err) 90 | } 91 | } else { 92 | d.SetId(response.UserId) 93 | d.Set("access_token", response.AccessToken) 94 | } 95 | } else { 96 | log.Println("[DEBUG] User whoami") 97 | response := &api.WhoAmIResponse{} 98 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/account/whoami") 99 | err := api.DoRequest("GET", urlStr, nil, response, accessTokenRaw.(string)) 100 | if err != nil { 101 | return fmt.Errorf("error performing whoami: %s", err) 102 | } 103 | 104 | d.SetId(response.UserId) 105 | } 106 | 107 | if displayNameRaw != nil { 108 | resourceUserSetDisplayName(d, meta, displayNameRaw.(string)) 109 | } 110 | 111 | if avatarMxcRaw != nil { 112 | resourceUserSetAvatarMxc(d, meta, avatarMxcRaw.(string)) 113 | } 114 | 115 | return resourceUserRead(d, meta) 116 | } 117 | 118 | func resourceUserExists(d *schema.ResourceData, m interface{}) (bool, error) { 119 | meta := m.(Metadata) 120 | 121 | accessToken := d.Get("access_token").(string) 122 | log.Println("[DEBUG] Doing whoami on:", d.Id()) 123 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/account/whoami") 124 | response := &api.WhoAmIResponse{} 125 | err := api.DoRequest("GET", urlStr, nil, response, accessToken) 126 | if err != nil { 127 | if mtxErr, ok := err.(*api.ErrorResponse); ok && mtxErr.ErrorCode == api.ErrCodeUnknownToken { 128 | // Mark as deleted 129 | return false, nil 130 | } 131 | return true, fmt.Errorf("error performing whoami: %s", err) 132 | } 133 | 134 | if response.UserId != d.Id() { 135 | // Mark as deleted 136 | return false, nil 137 | } 138 | 139 | return true, nil 140 | } 141 | 142 | func resourceUserRead(d *schema.ResourceData, m interface{}) error { 143 | meta := m.(Metadata) 144 | 145 | userId := d.Id() 146 | accessToken := d.Get("access_token").(string) 147 | 148 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/profile/", userId) 149 | log.Println("[DEBUG] Getting user profile:", urlStr) 150 | response := &api.ProfileResponse{} 151 | err := api.DoRequest("GET", urlStr, nil, response, accessToken) 152 | if err != nil { 153 | if mtxErr, ok := err.(*api.ErrorResponse); ok && mtxErr.ErrorCode == api.ErrCodeUnknownToken { 154 | // Mark as deleted 155 | d.SetId("") 156 | d.Set("access_token", "") 157 | return nil 158 | } 159 | return fmt.Errorf("error getting user profile: %s", err) 160 | } 161 | 162 | d.Set("display_name", response.DisplayName) 163 | d.Set("avatar_mxc", response.AvatarMxc) 164 | 165 | return nil 166 | } 167 | 168 | func resourceUserUpdate(d *schema.ResourceData, m interface{}) error { 169 | meta := m.(Metadata) 170 | 171 | if d.HasChange("avatar_mxc") { 172 | newMxc := d.Get("avatar_mxc").(string) 173 | err := resourceUserSetAvatarMxc(d, meta, newMxc) 174 | if err != nil { 175 | return err 176 | } 177 | } 178 | 179 | if d.HasChange("display_name") { 180 | newName := d.Get("display_name").(string) 181 | err := resourceUserSetDisplayName(d, meta, newName) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | 187 | return resourceUserRead(d, meta) 188 | } 189 | 190 | func resourceUserDelete(d *schema.ResourceData, m interface{}) error { 191 | // Users cannot be deleted in matrix, so we just say we deleted them 192 | return nil 193 | } 194 | 195 | func resourceUserSetDisplayName(d *schema.ResourceData, meta Metadata, newDisplayName string) error { 196 | accessToken := d.Get("access_token").(string) 197 | userId := d.Id() 198 | 199 | response := &api.ProfileUpdateResponse{} 200 | request := &api.ProfileDisplayNameRequest{DisplayName: newDisplayName} 201 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/profile/", userId, "/displayname") 202 | log.Println("[DEBUG] Updating user display name:", urlStr) 203 | err := api.DoRequest("PUT", urlStr, request, response, accessToken) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | return nil 209 | } 210 | 211 | func resourceUserSetAvatarMxc(d *schema.ResourceData, meta Metadata, newAvatarMxc string) error { 212 | accessToken := d.Get("access_token").(string) 213 | userId := d.Id() 214 | 215 | response := &api.ProfileUpdateResponse{} 216 | request := &api.ProfileAvatarUrlRequest{AvatarMxc: newAvatarMxc} 217 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/profile/", userId, "/avatar_url") 218 | log.Println("[DEBUG] Updating user avatar:", urlStr) 219 | err := api.DoRequest("PUT", urlStr, request, response, accessToken) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /matrix/resource_matrix_user_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | "github.com/hashicorp/terraform/helper/resource" 6 | "fmt" 7 | "github.com/turt2live/terraform-provider-matrix/matrix/api" 8 | "github.com/hashicorp/terraform/terraform" 9 | "regexp" 10 | ) 11 | 12 | type testAccMatrixUser struct { 13 | Profile *api.ProfileResponse 14 | UserId string 15 | } 16 | 17 | // HACK: This test assumes the localpart (username) becomes the user ID for the user. 18 | // From the spec: Matrix clients MUST NOT assume that localpart of the registered user_id matches the provided username. 19 | 20 | var testAccMatrixUserConfig_usernamePassword = ` 21 | resource "matrix_user" "foobar" { 22 | username = "foobar" 23 | password = "test1234" 24 | }` 25 | 26 | func TestAccMatrixUser_UsernamePassword(t *testing.T) { 27 | var meta testAccMatrixUser 28 | 29 | resource.Test(t, resource.TestCase{ 30 | PreCheck: func() { testAccPreCheck(t) }, 31 | Providers: testAccProviders, 32 | // We don't check if users get destroyed because they aren't 33 | //CheckDestroy: testAccCheckMatrixUserDestroy, 34 | Steps: []resource.TestStep{ 35 | { 36 | Config: testAccMatrixUserConfig_usernamePassword, 37 | Check: resource.ComposeTestCheckFunc( 38 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 39 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 40 | testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 41 | resource.TestMatchResourceAttr("matrix_user.foobar", "id", regexp.MustCompile("^@foobar:.*")), 42 | resource.TestMatchResourceAttr("matrix_user.foobar", "access_token", regexp.MustCompile(".+")), 43 | resource.TestCheckResourceAttr("matrix_user.foobar", "username", "foobar"), 44 | resource.TestCheckResourceAttr("matrix_user.foobar", "password", "test1234"), 45 | // we can't check the display name or avatar url because the homeserver might set it to something 46 | ), 47 | }, 48 | }, 49 | }) 50 | } 51 | 52 | var testAccMatrixUserConfig_usernamePasswordProfile = ` 53 | resource "matrix_user" "foobar" { 54 | username = "foobar" 55 | password = "test1234" 56 | display_name = "TEST_DISPLAY_NAME" 57 | avatar_mxc = "mxc://localhost/FakeAvatar" 58 | }` 59 | 60 | func TestAccMatrixUser_UsernamePasswordProfile(t *testing.T) { 61 | var meta testAccMatrixUser 62 | 63 | resource.Test(t, resource.TestCase{ 64 | PreCheck: func() { testAccPreCheck(t) }, 65 | Providers: testAccProviders, 66 | // We don't check if users get destroyed because they aren't 67 | //CheckDestroy: testAccCheckMatrixUserDestroy, 68 | Steps: []resource.TestStep{ 69 | { 70 | Config: testAccMatrixUserConfig_usernamePasswordProfile, 71 | Check: resource.ComposeTestCheckFunc( 72 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 73 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 74 | testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 75 | testAccCheckMatrixUserDisplayNameMatches("matrix_user.foobar", &meta), 76 | testAccCheckMatrixUserAvatarMxcMatches("matrix_user.foobar", &meta), 77 | resource.TestMatchResourceAttr("matrix_user.foobar", "id", regexp.MustCompile("^@foobar:.*")), 78 | resource.TestMatchResourceAttr("matrix_user.foobar", "access_token", regexp.MustCompile(".+")), 79 | resource.TestCheckResourceAttr("matrix_user.foobar", "username", "foobar"), 80 | resource.TestCheckResourceAttr("matrix_user.foobar", "password", "test1234"), 81 | resource.TestCheckResourceAttr("matrix_user.foobar", "display_name", "TEST_DISPLAY_NAME"), 82 | resource.TestCheckResourceAttr("matrix_user.foobar", "avatar_mxc", "mxc://localhost/FakeAvatar"), 83 | ), 84 | }, 85 | }, 86 | }) 87 | } 88 | 89 | var testAccMatrixUserConfig_accessToken = ` 90 | resource "matrix_user" "foobar" { 91 | access_token = "%s" 92 | }` 93 | 94 | func TestAccMatrixUser_AccessToken(t *testing.T) { 95 | var meta testAccMatrixUser 96 | testUser := testAccCreateTestUser("test_user_access_token") 97 | conf := fmt.Sprintf(testAccMatrixUserConfig_accessToken, testUser.AccessToken) 98 | 99 | resource.Test(t, resource.TestCase{ 100 | PreCheck: func() { testAccPreCheck(t) }, 101 | Providers: testAccProviders, 102 | // We don't check if users get destroyed because they aren't 103 | //CheckDestroy: testAccCheckMatrixUserDestroy, 104 | Steps: []resource.TestStep{ 105 | { 106 | Config: conf, 107 | Check: resource.ComposeTestCheckFunc( 108 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 109 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 110 | //testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 111 | testAccCheckMatrixUserDisplayNameMatches("matrix_user.foobar", &meta), 112 | testAccCheckMatrixUserAvatarMxcMatches("matrix_user.foobar", &meta), 113 | resource.TestCheckResourceAttr("matrix_user.foobar", "id", testUser.UserId), 114 | resource.TestCheckResourceAttr("matrix_user.foobar", "access_token", testUser.AccessToken), 115 | resource.TestCheckResourceAttr("matrix_user.foobar", "display_name", testUser.DisplayName), 116 | resource.TestCheckResourceAttr("matrix_user.foobar", "avatar_mxc", testUser.AvatarMxc), 117 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "username"), 118 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "password"), 119 | ), 120 | }, 121 | }, 122 | }) 123 | } 124 | 125 | var testAccMatrixUserConfig_accessTokenProfile = ` 126 | resource "matrix_user" "foobar" { 127 | access_token = "%s" 128 | display_name = "%s" 129 | avatar_mxc = "%s" 130 | }` 131 | 132 | func TestAccMatrixUser_AccessTokenProfile(t *testing.T) { 133 | var meta testAccMatrixUser 134 | testUser := testAccCreateTestUser("test_user_access_token_profile") 135 | 136 | // We cheat and set the properties here to make sure they'll match the checks later on 137 | testUser.DisplayName = "TESTING1234" 138 | testUser.AvatarMxc = "mxc://localhost/SomeMediaID" 139 | 140 | conf := fmt.Sprintf(testAccMatrixUserConfig_accessTokenProfile, testUser.AccessToken, testUser.DisplayName, testUser.AvatarMxc) 141 | 142 | resource.Test(t, resource.TestCase{ 143 | PreCheck: func() { testAccPreCheck(t) }, 144 | Providers: testAccProviders, 145 | // We don't check if users get destroyed because they aren't 146 | //CheckDestroy: testAccCheckMatrixUserDestroy, 147 | Steps: []resource.TestStep{ 148 | { 149 | Config: conf, 150 | Check: resource.ComposeTestCheckFunc( 151 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 152 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 153 | //testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 154 | testAccCheckMatrixUserDisplayNameMatches("matrix_user.foobar", &meta), 155 | testAccCheckMatrixUserAvatarMxcMatches("matrix_user.foobar", &meta), 156 | resource.TestCheckResourceAttr("matrix_user.foobar", "id", testUser.UserId), 157 | resource.TestCheckResourceAttr("matrix_user.foobar", "access_token", testUser.AccessToken), 158 | resource.TestCheckResourceAttr("matrix_user.foobar", "display_name", testUser.DisplayName), 159 | resource.TestCheckResourceAttr("matrix_user.foobar", "avatar_mxc", testUser.AvatarMxc), 160 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "username"), 161 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "password"), 162 | ), 163 | }, 164 | }, 165 | }) 166 | } 167 | 168 | var testAccMatrixUserConfig_updateProfile = ` 169 | resource "matrix_user" "foobar" { 170 | access_token = "%s" 171 | display_name = "%s" 172 | avatar_mxc = "%s" 173 | }` 174 | 175 | func TestAccMatrixUser_UpdateProfile(t *testing.T) { 176 | var meta testAccMatrixUser 177 | originalUser := testAccCreateTestUser("test_user_update_profile") 178 | 179 | // We cheat and set the properties here to make sure they'll match the checks later on 180 | originalUser.DisplayName = "TESTING1234" 181 | originalUser.AvatarMxc = "mxc://localhost/SomeMediaID" 182 | 183 | updatedUser := &test_MatrixUser{ 184 | UserId: originalUser.UserId, 185 | AvatarMxc: "mxc://localhost/SomeOtherMediaId", 186 | DisplayName: "New Display Name", 187 | AccessToken: originalUser.AccessToken, 188 | Localpart: originalUser.Localpart, 189 | Password: originalUser.Password, 190 | } 191 | 192 | confPart1 := fmt.Sprintf(testAccMatrixUserConfig_updateProfile, originalUser.AccessToken, originalUser.DisplayName, originalUser.AvatarMxc) 193 | confPart2 := fmt.Sprintf(testAccMatrixUserConfig_updateProfile, updatedUser.AccessToken, updatedUser.DisplayName, updatedUser.AvatarMxc) 194 | 195 | resource.Test(t, resource.TestCase{ 196 | PreCheck: func() { testAccPreCheck(t) }, 197 | Providers: testAccProviders, 198 | // We don't check if users get destroyed because they aren't 199 | //CheckDestroy: testAccCheckMatrixUserDestroy, 200 | Steps: []resource.TestStep{ 201 | { 202 | Config: confPart1, 203 | Check: resource.ComposeTestCheckFunc( 204 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 205 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 206 | //testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 207 | testAccCheckMatrixUserDisplayNameMatches("matrix_user.foobar", &meta), 208 | testAccCheckMatrixUserAvatarMxcMatches("matrix_user.foobar", &meta), 209 | resource.TestCheckResourceAttr("matrix_user.foobar", "id", originalUser.UserId), 210 | resource.TestCheckResourceAttr("matrix_user.foobar", "access_token", originalUser.AccessToken), 211 | resource.TestCheckResourceAttr("matrix_user.foobar", "display_name", originalUser.DisplayName), 212 | resource.TestCheckResourceAttr("matrix_user.foobar", "avatar_mxc", originalUser.AvatarMxc), 213 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "username"), 214 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "password"), 215 | ), 216 | }, 217 | { 218 | Config: confPart2, 219 | Check: resource.ComposeTestCheckFunc( 220 | testAccCheckMatrixUserExists("matrix_user.foobar", &meta), 221 | testAccCheckMatrixUserIdMatches("matrix_user.foobar", &meta), 222 | //testAccCheckMatrixUserAccessTokenWorks("matrix_user.foobar", &meta), 223 | testAccCheckMatrixUserDisplayNameMatches("matrix_user.foobar", &meta), 224 | testAccCheckMatrixUserAvatarMxcMatches("matrix_user.foobar", &meta), 225 | resource.TestCheckResourceAttr("matrix_user.foobar", "id", updatedUser.UserId), 226 | resource.TestCheckResourceAttr("matrix_user.foobar", "access_token", updatedUser.AccessToken), 227 | resource.TestCheckResourceAttr("matrix_user.foobar", "display_name", updatedUser.DisplayName), 228 | resource.TestCheckResourceAttr("matrix_user.foobar", "avatar_mxc", updatedUser.AvatarMxc), 229 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "username"), 230 | resource.TestCheckNoResourceAttr("matrix_user.foobar", "password"), 231 | ), 232 | }, 233 | }, 234 | }) 235 | } 236 | 237 | func testAccCheckMatrixUserExists(n string, user *testAccMatrixUser) resource.TestCheckFunc { 238 | return func(s *terraform.State) error { 239 | meta := testAccProvider.Meta().(Metadata) 240 | rs, ok := s.RootModule().Resources[n] 241 | 242 | if !ok { 243 | return fmt.Errorf("not found: %s", n) 244 | } 245 | 246 | if rs.Primary.ID == "" { 247 | return fmt.Errorf("record id not set") 248 | } 249 | 250 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/admin/whois/", rs.Primary.ID) 251 | response1 := &api.AdminWhoisResponse{} 252 | err := api.DoRequest("GET", urlStr, nil, response1, testAccAdminToken()) 253 | if err != nil { 254 | return err 255 | } 256 | 257 | urlStr = api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/profile/", rs.Primary.ID) 258 | response2 := &api.ProfileResponse{} 259 | err = api.DoRequest("GET", urlStr, nil, response2, testAccAdminToken()) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | *user = testAccMatrixUser{ 265 | UserId: response1.UserId, 266 | Profile: response2, 267 | } 268 | return nil 269 | } 270 | } 271 | 272 | func testAccCheckMatrixUserIdMatches(n string, user *testAccMatrixUser) resource.TestCheckFunc { 273 | return func(s *terraform.State) error { 274 | //meta := testAccProvider.Meta().(Metadata) 275 | rs, ok := s.RootModule().Resources[n] 276 | 277 | if !ok { 278 | return fmt.Errorf("not found: %s", n) 279 | } 280 | 281 | if rs.Primary.ID == "" { 282 | return fmt.Errorf("record id not set") 283 | } 284 | 285 | if rs.Primary.ID != user.UserId { 286 | return fmt.Errorf("user id doesn't match. expected: %s got: %s", user.UserId, rs.Primary.ID) 287 | } 288 | 289 | return nil 290 | } 291 | } 292 | 293 | func testAccCheckMatrixUserDisplayNameMatches(n string, user *testAccMatrixUser) resource.TestCheckFunc { 294 | return func(s *terraform.State) error { 295 | //meta := testAccProvider.Meta().(Metadata) 296 | rs, ok := s.RootModule().Resources[n] 297 | 298 | if !ok { 299 | return fmt.Errorf("not found: %s", n) 300 | } 301 | 302 | if rs.Primary.ID == "" { 303 | return fmt.Errorf("record id not set") 304 | } 305 | 306 | displayNameRaw := nilIfEmptyString(rs.Primary.Attributes["display_name"]) 307 | if displayNameRaw != user.Profile.DisplayName { 308 | return fmt.Errorf("display name doesn't match. exepcted: %s got: %s", user.Profile.DisplayName, displayNameRaw) 309 | } 310 | 311 | return nil 312 | } 313 | } 314 | 315 | func testAccCheckMatrixUserAvatarMxcMatches(n string, user *testAccMatrixUser) resource.TestCheckFunc { 316 | return func(s *terraform.State) error { 317 | //meta := testAccProvider.Meta().(Metadata) 318 | rs, ok := s.RootModule().Resources[n] 319 | 320 | if !ok { 321 | return fmt.Errorf("not found: %s", n) 322 | } 323 | 324 | if rs.Primary.ID == "" { 325 | return fmt.Errorf("record id not set") 326 | } 327 | 328 | avatarMxcRaw := nilIfEmptyString(rs.Primary.Attributes["avatar_mxc"]) 329 | if avatarMxcRaw != user.Profile.AvatarMxc { 330 | return fmt.Errorf("display name doesn't match. exepcted: %s got: %s", user.Profile.AvatarMxc, avatarMxcRaw) 331 | } 332 | 333 | return nil 334 | } 335 | } 336 | 337 | func testAccCheckMatrixUserAccessTokenWorks(n string, user *testAccMatrixUser) resource.TestCheckFunc { 338 | return func(s *terraform.State) error { 339 | meta := testAccProvider.Meta().(Metadata) 340 | rs, ok := s.RootModule().Resources[n] 341 | 342 | if !ok { 343 | return fmt.Errorf("not found: %s", n) 344 | } 345 | 346 | if rs.Primary.ID == "" { 347 | return fmt.Errorf("record id not set") 348 | } 349 | 350 | accessTokenRaw := nilIfEmptyString(rs.Primary.Attributes["access_token"]) 351 | 352 | response := &api.WhoAmIResponse{} 353 | urlStr := api.MakeUrl(meta.ClientApiUrl, "/_matrix/client/r0/account/whoami") 354 | err := api.DoRequest("GET", urlStr, nil, response, accessTokenRaw.(string)) 355 | if err != nil { 356 | return fmt.Errorf("error performing whoami: %s", err) 357 | } 358 | 359 | if response.UserId != rs.Primary.ID { 360 | return fmt.Errorf("whoami succeeded, although the user id does not match. expected: %s got: %s", rs.Primary.ID, response.UserId) 361 | } 362 | 363 | return nil 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /matrix/utils.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "strings" 5 | "fmt" 6 | "github.com/hashicorp/terraform/helper/schema" 7 | ) 8 | 9 | func nilIfEmptyString(val interface{}) interface{} { 10 | if val == "" { 11 | return nil 12 | } 13 | return val 14 | } 15 | 16 | func stripMxc(input string) (string, string, string, error) { 17 | if !strings.HasPrefix(input, "mxc://") { 18 | return "", "", "", fmt.Errorf("invalid mxc: missing protocol") 19 | } 20 | 21 | withoutProto := strings.TrimSpace(input[len("mxc://"):]) 22 | withoutProto = strings.Split(withoutProto, "?")[0] // Strip query string 23 | withoutProto = strings.Split(withoutProto, "#")[0] // Strip fragment 24 | if len(withoutProto) == 0 { 25 | return "", "", "", fmt.Errorf("invalid mxc: no origin or media_id") 26 | } 27 | 28 | parts := strings.Split(withoutProto, "/") 29 | if len(parts) != 2 { 30 | return "", "", "", fmt.Errorf("invalid mxc: wrong number of segments. expected: %d got: %d", 2, len(parts)) 31 | } 32 | 33 | origin := parts[0] 34 | mediaId := parts[1] 35 | constructed := fmt.Sprintf("mxc://%s/%s", origin, mediaId) 36 | return constructed, origin, mediaId, nil 37 | } 38 | 39 | func setOfStrings(val *schema.Set) []string { 40 | res := make([]string, 0) 41 | 42 | if val != nil { 43 | for _, v := range val.List() { 44 | res = append(res, v.(string)) 45 | } 46 | } 47 | 48 | return res 49 | } 50 | 51 | func getDomainName(identifier string) (string, error) { 52 | idParts := strings.Split(identifier, ":") 53 | if len(idParts) != 2 && len(idParts) != 3 { 54 | return "", fmt.Errorf("illegal matrix identifier: %s", identifier) 55 | } 56 | hsDomain := idParts[1] 57 | if len(idParts) > 2 { // port 58 | hsDomain = fmt.Sprintf("%s:%s", hsDomain, idParts[2]) 59 | } 60 | 61 | return hsDomain, nil 62 | } 63 | -------------------------------------------------------------------------------- /matrix/utils_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func TestUnitUtilsNilIfEmpty_nilWhenEmpty(t *testing.T) { 9 | r := nilIfEmptyString("") 10 | if r != nil { 11 | t.Errorf("result was not nil, got %#v, expected nil", r) 12 | } 13 | } 14 | 15 | func TestUnitUtilsNilIfEmpty_nilWhenNil(t *testing.T) { 16 | r := nilIfEmptyString(nil) 17 | if r != nil { 18 | t.Errorf("result was not nil, got %#v, expected nil", r) 19 | } 20 | } 21 | 22 | func TestUnitUtilsNilIfEmpty_stringWhenNotEmpty(t *testing.T) { 23 | r := nilIfEmptyString("hello") 24 | if r != "hello" { 25 | t.Errorf("result was not nil, got %#v, expected 'hello'", r) 26 | } 27 | } 28 | 29 | func TestUnitUtilsNilIfEmpty_objectWhenObject(t *testing.T) { 30 | v := struct{}{} 31 | r := nilIfEmptyString(v) 32 | if r != v { 33 | t.Errorf("result was not nil, got %#v, expected %#v", r, v) 34 | } 35 | } 36 | 37 | func TestUnitUtilsStripMxc_validMxc(t *testing.T) { 38 | full, origin, mediaId, err := stripMxc("mxc://host.name/some_media") 39 | if err != nil { 40 | t.Errorf("unexpected error: %s", err) 41 | } 42 | 43 | if full != "mxc://host.name/some_media" { 44 | t.Errorf("wrong mxc, got: %s expected: %s", full, "mxc://host.name/some_media") 45 | } 46 | 47 | if origin != "host.name" { 48 | t.Errorf("wrong origin, got: %s expected: %s", origin, "host.name") 49 | } 50 | 51 | if mediaId != "some_media" { 52 | t.Errorf("wrong media_id, got: %s expected: %s", mediaId, "some_media") 53 | } 54 | } 55 | 56 | func TestUnitUtilsStripMxc_stripsQueryString(t *testing.T) { 57 | full, origin, mediaId, err := stripMxc("mxc://host.name/some_media?query=val") 58 | if err != nil { 59 | t.Errorf("unexpected error: %s", err) 60 | } 61 | 62 | if full != "mxc://host.name/some_media" { 63 | t.Errorf("wrong mxc, got: %s expected: %s", full, "mxc://host.name/some_media") 64 | } 65 | 66 | if origin != "host.name" { 67 | t.Errorf("wrong origin, got: %s expected: %s", origin, "host.name") 68 | } 69 | 70 | if mediaId != "some_media" { 71 | t.Errorf("wrong media_id, got: %s expected: %s", mediaId, "some_media") 72 | } 73 | } 74 | 75 | func TestUnitUtilsStripMxc_stripsFragment(t *testing.T) { 76 | full, origin, mediaId, err := stripMxc("mxc://host.name/some_media#fragment") 77 | if err != nil { 78 | t.Errorf("unexpected error: %s", err) 79 | } 80 | 81 | if full != "mxc://host.name/some_media" { 82 | t.Errorf("wrong mxc, got: %s expected: %s", full, "mxc://host.name/some_media") 83 | } 84 | 85 | if origin != "host.name" { 86 | t.Errorf("wrong origin, got: %s expected: %s", origin, "host.name") 87 | } 88 | 89 | if mediaId != "some_media" { 90 | t.Errorf("wrong media_id, got: %s expected: %s", mediaId, "some_media") 91 | } 92 | } 93 | 94 | func TestUnitUtilsStripMxc_errNoProto(t *testing.T) { 95 | _, _, _, err := stripMxc("invalid") 96 | if err == nil { 97 | t.Errorf("unexpected lack of error") 98 | } 99 | 100 | if err.Error() != "invalid mxc: missing protocol" { 101 | t.Errorf("unexpected error message, got: %s expected: %s", err, "invalid mxc: missing protocol") 102 | } 103 | } 104 | 105 | func TestUnitUtilsStripMxc_errNoLength(t *testing.T) { 106 | _, _, _, err := stripMxc("mxc://") 107 | if err == nil { 108 | t.Errorf("unexpected lack of error") 109 | } 110 | 111 | if err.Error() != "invalid mxc: no origin or media_id" { 112 | t.Errorf("unexpected error message, got: %s expected: %s", err, "invalid mxc: no origin or media_id") 113 | } 114 | } 115 | 116 | func TestUnitUtilsStripMxc_errExtraSegments(t *testing.T) { 117 | _, _, _, err := stripMxc("mxc://one/two/three") 118 | if err == nil { 119 | t.Errorf("unexpected lack of error") 120 | } 121 | 122 | if err.Error() != "invalid mxc: wrong number of segments. expected: 2 got: 3" { 123 | t.Errorf("unexpected error message, got: %s expected: %s", err, "invalid mxc: wrong number of segments. expected: 2 got: 3") 124 | } 125 | } 126 | 127 | func TestUnitUtilsStripMxc_errMissingSegments(t *testing.T) { 128 | _, _, _, err := stripMxc("mxc://one") 129 | if err == nil { 130 | t.Errorf("unexpected lack of error") 131 | } 132 | 133 | if err.Error() != "invalid mxc: wrong number of segments. expected: 2 got: 1" { 134 | t.Errorf("unexpected error message, got: %s expected: %s", err, "invalid mxc: wrong number of segments. expected: 2 got: 1") 135 | } 136 | } 137 | 138 | func TestUnitUtilsSetOfStrings_producesStringArray(t *testing.T) { 139 | input := []interface{}{"A", "B", "C"} 140 | set := schema.NewSet(schema.HashString, input) 141 | result := setOfStrings(set) 142 | 143 | if result == nil { 144 | t.Errorf("result is nil") 145 | } 146 | 147 | if len(result) != len(input) { 148 | t.Errorf("unexpected length. expected: %d got: %d", len(input), len(result)) 149 | } 150 | 151 | // Sets are unordered, so we have to confirm the contents exist 152 | found := make([]bool, len(input)) 153 | for i, expected := range input { 154 | for _, actual := range result { 155 | if expected == actual { 156 | alreadyFound := found[i] 157 | if alreadyFound { 158 | t.Errorf("duplicate entry discovered at index %d of source", i) 159 | } 160 | found[i] = true 161 | } 162 | } 163 | } 164 | for i, v := range found { 165 | if !v { 166 | t.Errorf("failed to find index %d of source in result", i) 167 | } 168 | } 169 | } 170 | 171 | func TestUnitUtilsSetOfStrings_emptyWhenNil(t *testing.T) { 172 | result := setOfStrings(nil) 173 | 174 | if result == nil { 175 | t.Errorf("result is nil") 176 | } 177 | 178 | if len(result) != 0 { 179 | t.Errorf("unexpected length. expected: %d got: %d", 0, len(result)) 180 | } 181 | } 182 | 183 | func TestUnitUtilsGetDomainName_parsesUserId(t *testing.T) { 184 | result, err := getDomainName("@user:domain.com") 185 | 186 | if err != nil { 187 | t.Errorf("unexpected error: %s", err) 188 | } 189 | 190 | if result != "domain.com" { 191 | t.Errorf("expected: %s got: %s", "domain.com", result) 192 | } 193 | } 194 | 195 | func TestUnitUtilsGetDomainName_parsesGroupId(t *testing.T) { 196 | result, err := getDomainName("+group:domain.com") 197 | 198 | if err != nil { 199 | t.Errorf("unexpected error: %s", err) 200 | } 201 | 202 | if result != "domain.com" { 203 | t.Errorf("expected: %s got: %s", "domain.com", result) 204 | } 205 | } 206 | 207 | func TestUnitUtilsGetDomainName_parsesAlias(t *testing.T) { 208 | result, err := getDomainName("#room:domain.com") 209 | 210 | if err != nil { 211 | t.Errorf("unexpected error: %s", err) 212 | } 213 | 214 | if result != "domain.com" { 215 | t.Errorf("expected: %s got: %s", "domain.com", result) 216 | } 217 | } 218 | 219 | func TestUnitUtilsGetDomainName_parsesPortNumber(t *testing.T) { 220 | result, err := getDomainName("#room:domain.com:8182") 221 | 222 | if err != nil { 223 | t.Errorf("unexpected error: %s", err) 224 | } 225 | 226 | if result != "domain.com:8182" { 227 | t.Errorf("expected: %s got: %s", "domain.com:8182", result) 228 | } 229 | } 230 | 231 | func TestUnitUtilsGetDomainName_parsesWithoutTld(t *testing.T) { 232 | result, err := getDomainName("#room:localhost") 233 | 234 | if err != nil { 235 | t.Errorf("unexpected error: %s", err) 236 | } 237 | 238 | if result != "localhost" { 239 | t.Errorf("expected: %s got: %s", "localhost", result) 240 | } 241 | } 242 | 243 | func TestUnitUtilsGetDomainName_errorWhenInvalid(t *testing.T) { 244 | _, err := getDomainName("#room") 245 | 246 | if err == nil { 247 | t.Errorf("expected an error, but got a result") 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t terraform-provider-matrix-tests . 4 | docker run --rm --name terraform-provider-matrix-tests -e TF_LOG=$TF_LOG terraform-provider-matrix-tests 5 | --------------------------------------------------------------------------------