mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-20 18:28:25 +02:00
Merge branch 'main' of github.com:mattermost/focalboard into main
This commit is contained in:
commit
3af92afadd
2
.gitignore
vendored
2
.gitignore
vendored
@ -65,6 +65,8 @@ webapp/cypress/screenshots
|
||||
webapp/cypress/videos
|
||||
server/swagger/clients
|
||||
server/vendor
|
||||
mattermost-plugin/vendor
|
||||
mattermost-plugin/dist
|
||||
.idea
|
||||
docker/certs
|
||||
docker/data
|
||||
|
@ -8,18 +8,15 @@
|
||||
"test_dbconfig": "file::memory:?cache=shared",
|
||||
"useSSL": false,
|
||||
"webpath": "./webapp/pack",
|
||||
"filesdriver": "local",
|
||||
"filespath": "./files",
|
||||
"telemetry": true,
|
||||
"prometheus_address": ":9092",
|
||||
"webhook_update": [],
|
||||
"secret": "this-is-a-secret-string",
|
||||
"session_expire_time": 2592000,
|
||||
"session_refresh_time": 18000,
|
||||
"localOnly": false,
|
||||
"enableLocalMode": true,
|
||||
"localModeSocketLocation": "/var/tmp/focalboard_local.socket",
|
||||
"authMode": "native",
|
||||
"mattermostURL": "",
|
||||
"mattermostClientID": "",
|
||||
"mattermostClientSecret": ""
|
||||
"authMode": "native"
|
||||
}
|
||||
|
@ -38,19 +38,16 @@ func runServer(port int) (*server.Server, error) {
|
||||
UseSSL: false,
|
||||
SecureCookie: true,
|
||||
WebPath: "./pack",
|
||||
FilesDriver: "local",
|
||||
FilesPath: "./focalboard_files",
|
||||
Telemetry: true,
|
||||
WebhookUpdate: []string{},
|
||||
Secret: "",
|
||||
SessionExpireTime: 259200000000,
|
||||
SessionRefreshTime: 18000,
|
||||
LocalOnly: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: "",
|
||||
AuthMode: "native",
|
||||
MattermostURL: "",
|
||||
MattermostClientID: "",
|
||||
MattermostClientSecret: "",
|
||||
}, sessionToken)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR INITIALIZING THE SERVER", err)
|
||||
|
12
mattermost-plugin/.circleci/config.yml
Normal file
12
mattermost-plugin/.circleci/config.yml
Normal file
@ -0,0 +1,12 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
plugin-ci: mattermost/plugin-ci@0.1.0
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- plugin-ci/lint
|
||||
- plugin-ci/test
|
||||
- plugin-ci/build
|
27
mattermost-plugin/.editorconfig
Normal file
27
mattermost-plugin/.editorconfig
Normal file
@ -0,0 +1,27 @@
|
||||
# http://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.{js, jsx, ts, tsx, json, html}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[webapp/package.json]
|
||||
indent_size = 2
|
||||
|
||||
[{Makefile, *.mk}]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
1
mattermost-plugin/.gitattributes
vendored
Normal file
1
mattermost-plugin/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
server/manifest.go linguist-generated=true
|
56
mattermost-plugin/.golangci.yml
Normal file
56
mattermost-plugin/.golangci.yml
Normal file
@ -0,0 +1,56 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes: github.com/mattermost/mattermost-starter-template
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gocritic
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: server/manifest.go
|
||||
linters:
|
||||
- deadcode
|
||||
- unused
|
||||
- varcheck
|
||||
- path: server/configuration.go
|
||||
linters:
|
||||
- unused
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- bodyclose
|
||||
- scopelint # https://github.com/kyoh86/scopelint/issues/4
|
201
mattermost-plugin/LICENSE
Normal file
201
mattermost-plugin/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
267
mattermost-plugin/Makefile
Normal file
267
mattermost-plugin/Makefile
Normal file
@ -0,0 +1,267 @@
|
||||
GO ?= $(shell command -v go 2> /dev/null)
|
||||
NPM ?= $(shell command -v npm 2> /dev/null)
|
||||
CURL ?= $(shell command -v curl 2> /dev/null)
|
||||
MM_DEBUG ?=
|
||||
MANIFEST_FILE ?= plugin.json
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
GO_TEST_FLAGS ?= -race
|
||||
GO_BUILD_FLAGS ?=
|
||||
MM_UTILITIES_DIR ?= ../mattermost-utilities
|
||||
DLV_DEBUG_PORT := 2346
|
||||
|
||||
export GO111MODULE=on
|
||||
|
||||
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures.
|
||||
ASSETS_DIR ?= assets
|
||||
|
||||
## Define the default target (make all)
|
||||
.PHONY: default
|
||||
default: all
|
||||
|
||||
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
|
||||
include build/setup.mk
|
||||
|
||||
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
|
||||
|
||||
# Include custom makefile, if present
|
||||
ifneq ($(wildcard build/custom.mk),)
|
||||
include build/custom.mk
|
||||
endif
|
||||
|
||||
## Checks the code style, tests, builds and bundles the plugin.
|
||||
.PHONY: all
|
||||
all: check-style test dist
|
||||
|
||||
## Propagates plugin manifest information into the server/ and webapp/ folders.
|
||||
.PHONY: apply
|
||||
apply:
|
||||
./build/bin/manifest apply
|
||||
|
||||
## Runs eslint and golangci-lint
|
||||
.PHONY: check-style
|
||||
check-style: webapp/node_modules
|
||||
@echo Checking for style guide compliance
|
||||
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && npm run lint
|
||||
cd webapp && npm run check-types
|
||||
endif
|
||||
|
||||
ifneq ($(HAS_SERVER),)
|
||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
|
||||
@echo Running golangci-lint
|
||||
golangci-lint run ./...
|
||||
endif
|
||||
|
||||
## Builds the server, if it exists, for all supported architectures.
|
||||
.PHONY: server
|
||||
server:
|
||||
ifneq ($(HAS_SERVER),)
|
||||
mkdir -p server/dist;
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd server && env GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-linux-amd64;
|
||||
cd server && env GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-darwin-amd64;
|
||||
cd server && env GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -o dist/plugin-windows-amd64.exe;
|
||||
else
|
||||
$(info DEBUG mode is on; to disable, unset MM_DEBUG)
|
||||
|
||||
cd server && env GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-darwin-amd64;
|
||||
cd server && env GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-linux-amd64;
|
||||
cd server && env GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) -gcflags "all=-N -l" -o dist/plugin-windows-amd64.exe;
|
||||
endif
|
||||
endif
|
||||
|
||||
## Ensures NPM dependencies are installed without having to run this all the time.
|
||||
webapp/node_modules: $(wildcard webapp/package.json)
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && $(NPM) install
|
||||
touch $@
|
||||
endif
|
||||
|
||||
## Builds the webapp, if it exists.
|
||||
.PHONY: webapp
|
||||
webapp: webapp/node_modules
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd webapp && $(NPM) run build;
|
||||
else
|
||||
cd webapp && $(NPM) run debug;
|
||||
endif
|
||||
endif
|
||||
|
||||
## Generates a tar bundle of the plugin for install.
|
||||
.PHONY: bundle
|
||||
bundle:
|
||||
rm -rf dist/
|
||||
mkdir -p dist/$(PLUGIN_ID)
|
||||
cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/
|
||||
cp -r ../webapp/pack dist/$(PLUGIN_ID)/
|
||||
ifneq ($(wildcard $(ASSETS_DIR)/.),)
|
||||
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
|
||||
endif
|
||||
ifneq ($(HAS_PUBLIC),)
|
||||
cp -r public dist/$(PLUGIN_ID)/
|
||||
endif
|
||||
ifneq ($(HAS_SERVER),)
|
||||
mkdir -p dist/$(PLUGIN_ID)/server
|
||||
cp -r server/dist dist/$(PLUGIN_ID)/server/
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
mkdir -p dist/$(PLUGIN_ID)/webapp
|
||||
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
|
||||
endif
|
||||
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
|
||||
|
||||
@echo plugin built at: dist/$(BUNDLE_NAME)
|
||||
|
||||
## Builds and bundles the plugin.
|
||||
.PHONY: dist
|
||||
dist: apply server webapp bundle
|
||||
|
||||
## Builds and installs the plugin to a server.
|
||||
.PHONY: deploy
|
||||
deploy: dist
|
||||
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
|
||||
|
||||
## Builds and installs the plugin to a server, updating the webapp automatically when changed.
|
||||
.PHONY: watch
|
||||
watch: apply server bundle
|
||||
ifeq ($(MM_DEBUG),)
|
||||
cd webapp && $(NPM) run build:watch
|
||||
else
|
||||
cd webapp && $(NPM) run debug:watch
|
||||
endif
|
||||
|
||||
## Installs a previous built plugin with updated webpack assets to a server.
|
||||
.PHONY: deploy-from-watch
|
||||
deploy-from-watch: bundle
|
||||
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
|
||||
|
||||
## Setup dlv for attaching, identifying the plugin PID for other targets.
|
||||
.PHONY: setup-attach
|
||||
setup-attach:
|
||||
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||
$(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w))
|
||||
|
||||
@if [ ${NUM_PID} -gt 2 ]; then \
|
||||
echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## Check if setup-attach succeeded.
|
||||
.PHONY: check-attach
|
||||
check-attach:
|
||||
@if [ -z ${PLUGIN_PID} ]; then \
|
||||
echo "Could not find plugin PID; the plugin is not running. Exiting."; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "Located Plugin running with PID: ${PLUGIN_PID}"; \
|
||||
fi
|
||||
|
||||
## Attach dlv to an existing plugin instance.
|
||||
.PHONY: attach
|
||||
attach: setup-attach check-attach
|
||||
dlv attach ${PLUGIN_PID}
|
||||
|
||||
## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT.
|
||||
.PHONY: attach-headless
|
||||
attach-headless: setup-attach check-attach
|
||||
dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient
|
||||
|
||||
## Detach dlv from an existing plugin instance, if previously attached.
|
||||
.PHONY: detach
|
||||
detach: setup-attach
|
||||
@DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \
|
||||
if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \
|
||||
echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \
|
||||
kill -9 $$DELVE_PID ; \
|
||||
fi
|
||||
|
||||
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||
.PHONY: test
|
||||
test: webapp/node_modules
|
||||
ifneq ($(HAS_SERVER),)
|
||||
$(GO) test -v $(GO_TEST_FLAGS) ./server/...
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
cd webapp && $(NPM) run test;
|
||||
endif
|
||||
ifneq ($(wildcard ./build/sync/plan/.),)
|
||||
cd ./build/sync && $(GO) test -v $(GO_TEST_FLAGS) ./...
|
||||
endif
|
||||
|
||||
## Creates a coverage report for the server code.
|
||||
.PHONY: coverage
|
||||
coverage: webapp/node_modules
|
||||
ifneq ($(HAS_SERVER),)
|
||||
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
|
||||
$(GO) tool cover -html=server/coverage.txt
|
||||
endif
|
||||
|
||||
## Extract strings for translation from the source code.
|
||||
.PHONY: i18n-extract
|
||||
i18n-extract:
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(HAS_MM_UTILITIES),)
|
||||
@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||
else
|
||||
cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
|
||||
endif
|
||||
endif
|
||||
|
||||
## Disable the plugin.
|
||||
.PHONY: disable
|
||||
disable: detach
|
||||
./build/bin/pluginctl disable $(PLUGIN_ID)
|
||||
|
||||
## Enable the plugin.
|
||||
.PHONY: enable
|
||||
enable:
|
||||
./build/bin/pluginctl enable $(PLUGIN_ID)
|
||||
|
||||
## Reset the plugin, effectively disabling and re-enabling it on the server.
|
||||
.PHONY: reset
|
||||
reset: detach
|
||||
./build/bin/pluginctl reset $(PLUGIN_ID)
|
||||
|
||||
## Kill all instances of the plugin, detaching any existing dlv instance.
|
||||
.PHONY: kill
|
||||
kill: detach
|
||||
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
|
||||
|
||||
@for PID in ${PLUGIN_PID}; do \
|
||||
echo "Killing plugin pid $$PID"; \
|
||||
kill -9 $$PID; \
|
||||
done; \
|
||||
|
||||
## Clean removes all build artifacts.
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -fr dist/
|
||||
ifneq ($(HAS_SERVER),)
|
||||
rm -fr server/coverage.txt
|
||||
rm -fr server/dist
|
||||
endif
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
rm -fr webapp/junit.xml
|
||||
rm -fr webapp/dist
|
||||
rm -fr webapp/node_modules
|
||||
endif
|
||||
rm -fr build/bin/
|
||||
|
||||
## Sync directory with a starter template
|
||||
sync:
|
||||
ifndef STARTERTEMPLATE_PATH
|
||||
@echo STARTERTEMPLATE_PATH is not set.
|
||||
@echo Set STARTERTEMPLATE_PATH to a local clone of https://github.com/mattermost/mattermost-plugin-starter-template and retry.
|
||||
@exit 1
|
||||
endif
|
||||
cd ${STARTERTEMPLATE_PATH} && go run ./build/sync/main.go ./build/sync/plan.yml $(PWD)
|
||||
|
||||
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort
|
3
mattermost-plugin/README.md
Normal file
3
mattermost-plugin/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Focalboard Plugin for Mattermost
|
||||
|
||||
This plugin allows to run focalboard inside your mattermost instance as a plugin.
|
0
mattermost-plugin/assets/.gitkeep
Normal file
0
mattermost-plugin/assets/.gitkeep
Normal file
14
mattermost-plugin/assets/starter-template-icon.svg
Normal file
14
mattermost-plugin/assets/starter-template-icon.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="241px" height="240px" viewBox="0 0 241 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>blue-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="06" transform="translate(-681.000000, -572.000000)" fill="#1875F0">
|
||||
<g id="Group-2" transform="translate(626.000000, 517.000000)">
|
||||
<path d="M216.908181,153.127705 C216.908181,153.127705 217.280588,169.452526 205.928754,180.543035 C194.57546,191.633544 180.631383,190.619887 171.560722,187.557072 C162.488602,184.494256 150.79503,176.85251 148.531381,161.16705 C146.269193,145.480133 156.508188,132.736607 156.508188,132.736607 L178.820463,105.066407 L191.815268,89.2629779 L202.969946,75.4912313 C202.969946,75.4912313 208.088713,68.6534193 209.547671,67.2421648 C209.836834,66.9625354 210.133299,66.7790286 210.423923,66.6377576 L210.635683,66.5299837 L210.673654,66.5154197 C211.28703,66.2518108 211.993873,66.195011 212.675888,66.4251227 C213.343299,66.6508652 213.860288,67.1081757 214.187421,67.6718037 L214.256061,67.7810339 L214.315938,67.9062846 C214.475124,68.2063036 214.608022,68.5485583 214.67082,68.9709151 C214.968745,70.976382 214.870897,79.5094471 214.870897,79.5094471 L215.342613,97.2047434 L216.039232,117.630795 L216.908181,153.127705 Z M245.790587,78.2043261 C287.057212,108.155253 305.982915,162.509669 288.774288,213.346872 C267.594104,275.911031 199.706245,309.46073 137.142925,288.281718 C74.5796048,267.10125 41.031812,199.213937 62.2105402,136.649778 C79.4482947,85.7295603 127.625459,54.0324057 178.690632,55.4145322 L162.322339,74.7541074 C132.028106,80.231639 105.87146,100.919843 95.5908489,131.290215 C80.2944535,176.475117 105.932628,225.982624 152.855846,241.866155 C199.777608,257.751142 250.216536,233.998666 265.512932,188.813764 C275.760046,158.543884 267.634882,126.336988 247.050359,103.595256 L245.790587,78.2043261 Z" id="blue-icon"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
1
mattermost-plugin/build/.gitignore
vendored
Normal file
1
mattermost-plugin/build/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
bin
|
1
mattermost-plugin/build/custom.mk
Normal file
1
mattermost-plugin/build/custom.mk
Normal file
@ -0,0 +1 @@
|
||||
# Include custom targets and environment variables here
|
11
mattermost-plugin/build/go.mod
Normal file
11
mattermost-plugin/build/go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module github.com/mattermost/mattermost-plugin-starter-template/build
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.1.0
|
||||
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200924100636-e726b0426826
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
1003
mattermost-plugin/build/go.sum
Normal file
1003
mattermost-plugin/build/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
126
mattermost-plugin/build/manifest/main.go
Normal file
126
mattermost-plugin/build/manifest/main.go
Normal file
@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
var manifest *model.Manifest
|
||||
|
||||
const manifestStr = ` + "`" + `
|
||||
%s
|
||||
` + "`" + `
|
||||
|
||||
func init() {
|
||||
manifest = model.ManifestFromJson(strings.NewReader(manifestStr))
|
||||
}
|
||||
`
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
panic("no cmd specified")
|
||||
}
|
||||
|
||||
manifest, err := findManifest()
|
||||
if err != nil {
|
||||
panic("failed to find manifest: " + err.Error())
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
switch cmd {
|
||||
case "id":
|
||||
dumpPluginID(manifest)
|
||||
|
||||
case "version":
|
||||
dumpPluginVersion(manifest)
|
||||
|
||||
case "has_server":
|
||||
if manifest.HasServer() {
|
||||
fmt.Printf("true")
|
||||
}
|
||||
|
||||
case "has_webapp":
|
||||
if manifest.HasWebapp() {
|
||||
fmt.Printf("true")
|
||||
}
|
||||
|
||||
case "apply":
|
||||
if err := applyManifest(manifest); err != nil {
|
||||
panic("failed to apply manifest: " + err.Error())
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unrecognized command: " + cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func findManifest() (*model.Manifest, error) {
|
||||
_, manifestFilePath, err := model.FindManifest(".")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find manifest in current working directory")
|
||||
}
|
||||
manifestFile, err := os.Open(manifestFilePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath)
|
||||
}
|
||||
defer manifestFile.Close()
|
||||
|
||||
// Re-decode the manifest, disallowing unknown fields. When we write the manifest back out,
|
||||
// we don't want to accidentally clobber anything we won't preserve.
|
||||
var manifest model.Manifest
|
||||
decoder := json.NewDecoder(manifestFile)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err = decoder.Decode(&manifest); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse manifest")
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// dumpPluginId writes the plugin id from the given manifest to standard out
|
||||
func dumpPluginID(manifest *model.Manifest) {
|
||||
fmt.Printf("%s", manifest.Id)
|
||||
}
|
||||
|
||||
// dumpPluginVersion writes the plugin version from the given manifest to standard out
|
||||
func dumpPluginVersion(manifest *model.Manifest) {
|
||||
fmt.Printf("%s", manifest.Version)
|
||||
}
|
||||
|
||||
// applyManifest propagates the plugin_id into the server and webapp folders, as necessary
|
||||
func applyManifest(manifest *model.Manifest) error {
|
||||
if manifest.HasServer() {
|
||||
// generate JSON representation of Manifest.
|
||||
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifestStr := string(manifestBytes)
|
||||
|
||||
// write generated code to file by using Go file template.
|
||||
if err := ioutil.WriteFile(
|
||||
"server/manifest.go",
|
||||
[]byte(fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)),
|
||||
0600,
|
||||
); err != nil {
|
||||
return errors.Wrap(err, "failed to write server/manifest.go")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
172
mattermost-plugin/build/pluginctl/main.go
Normal file
172
mattermost-plugin/build/pluginctl/main.go
Normal file
@ -0,0 +1,172 @@
|
||||
// main handles deployment of the plugin to a development server using the Client4 API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
const helpText = `
|
||||
Usage:
|
||||
pluginctl deploy <plugin id> <bundle path>
|
||||
pluginctl disable <plugin id>
|
||||
pluginctl enable <plugin id>
|
||||
pluginctl reset <plugin id>
|
||||
`
|
||||
|
||||
func main() {
|
||||
err := pluginctl()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed: %s\n", err.Error())
|
||||
fmt.Print(helpText)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func pluginctl() error {
|
||||
if len(os.Args) < 3 {
|
||||
return errors.New("invalid number of arguments")
|
||||
}
|
||||
|
||||
client, err := getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "deploy":
|
||||
if len(os.Args) < 4 {
|
||||
return errors.New("invalid number of arguments")
|
||||
}
|
||||
return deploy(client, os.Args[2], os.Args[3])
|
||||
case "disable":
|
||||
return disablePlugin(client, os.Args[2])
|
||||
case "enable":
|
||||
return enablePlugin(client, os.Args[2])
|
||||
case "reset":
|
||||
return resetPlugin(client, os.Args[2])
|
||||
default:
|
||||
return errors.New("invalid second argument")
|
||||
}
|
||||
}
|
||||
|
||||
func getClient() (*model.Client4, error) {
|
||||
socketPath := os.Getenv("MM_LOCALSOCKETPATH")
|
||||
if socketPath == "" {
|
||||
socketPath = model.LOCAL_MODE_SOCKET_PATH
|
||||
}
|
||||
|
||||
client, connected := getUnixClient(socketPath)
|
||||
if connected {
|
||||
log.Printf("Connecting using local mode over %s", socketPath)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
||||
log.Printf("No socket found at %s for local mode deployment. Attempting to authenticate with credentials.", socketPath)
|
||||
}
|
||||
|
||||
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
||||
adminToken := os.Getenv("MM_ADMIN_TOKEN")
|
||||
adminUsername := os.Getenv("MM_ADMIN_USERNAME")
|
||||
adminPassword := os.Getenv("MM_ADMIN_PASSWORD")
|
||||
|
||||
if siteURL == "" {
|
||||
return nil, errors.New("MM_SERVICESETTINGS_SITEURL is not set")
|
||||
}
|
||||
|
||||
client = model.NewAPIv4Client(siteURL)
|
||||
|
||||
if adminToken != "" {
|
||||
log.Printf("Authenticating using token against %s.", siteURL)
|
||||
client.SetToken(adminToken)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
if adminUsername != "" && adminPassword != "" {
|
||||
client := model.NewAPIv4Client(siteURL)
|
||||
log.Printf("Authenticating as %s against %s.", adminUsername, siteURL)
|
||||
_, resp := client.Login(adminUsername, adminPassword)
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, resp.Error)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("one of MM_ADMIN_TOKEN or MM_ADMIN_USERNAME/MM_ADMIN_PASSWORD must be defined")
|
||||
}
|
||||
|
||||
func getUnixClient(socketPath string) (*model.Client4, bool) {
|
||||
_, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return model.NewAPIv4SocketClient(socketPath), true
|
||||
}
|
||||
|
||||
// deploy attempts to upload and enable a plugin via the Client4 API.
|
||||
// It will fail if plugin uploads are disabled.
|
||||
func deploy(client *model.Client4, pluginID, bundlePath string) error {
|
||||
pluginBundle, err := os.Open(bundlePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s: %w", bundlePath, err)
|
||||
}
|
||||
defer pluginBundle.Close()
|
||||
|
||||
log.Print("Uploading plugin via API.")
|
||||
_, resp := client.UploadPluginForced(pluginBundle)
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("failed to upload plugin bundle: %s", resp.Error.Error())
|
||||
}
|
||||
|
||||
log.Print("Enabling plugin.")
|
||||
_, resp = client.EnablePlugin(pluginID)
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("failed to enable plugin: %s", resp.Error.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// disablePlugin attempts to disable the plugin via the Client4 API.
|
||||
func disablePlugin(client *model.Client4, pluginID string) error {
|
||||
log.Print("Disabling plugin.")
|
||||
_, resp := client.DisablePlugin(pluginID)
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("failed to disable plugin: %w", resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enablePlugin attempts to enable the plugin via the Client4 API.
|
||||
func enablePlugin(client *model.Client4, pluginID string) error {
|
||||
log.Print("Enabling plugin.")
|
||||
_, resp := client.EnablePlugin(pluginID)
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("failed to enable plugin: %w", resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetPlugin attempts to reset the plugin via the Client4 API.
|
||||
func resetPlugin(client *model.Client4, pluginID string) error {
|
||||
err := disablePlugin(client, pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = enablePlugin(client, pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
45
mattermost-plugin/build/setup.mk
Normal file
45
mattermost-plugin/build/setup.mk
Normal file
@ -0,0 +1,45 @@
|
||||
# Ensure that go is installed. Note that this is independent of whether or not a server is being
|
||||
# built, since the build script itself uses go.
|
||||
ifeq ($(GO),)
|
||||
$(error "go is not available: see https://golang.org/doc/install")
|
||||
endif
|
||||
|
||||
# Ensure that the build tools are compiled. Go's caching makes this quick.
|
||||
$(shell cd build/manifest && $(GO) build -o ../bin/manifest)
|
||||
|
||||
# Ensure that the deployment tools are compiled. Go's caching makes this quick.
|
||||
$(shell cd build/pluginctl && $(GO) build -o ../bin/pluginctl)
|
||||
|
||||
# Extract the plugin id from the manifest.
|
||||
PLUGIN_ID ?= $(shell build/bin/manifest id)
|
||||
ifeq ($(PLUGIN_ID),)
|
||||
$(error "Cannot parse id from $(MANIFEST_FILE)")
|
||||
endif
|
||||
|
||||
# Extract the plugin version from the manifest.
|
||||
PLUGIN_VERSION ?= $(shell build/bin/manifest version)
|
||||
ifeq ($(PLUGIN_VERSION),)
|
||||
$(error "Cannot parse version from $(MANIFEST_FILE)")
|
||||
endif
|
||||
|
||||
# Determine if a server is defined in the manifest.
|
||||
HAS_SERVER ?= $(shell build/bin/manifest has_server)
|
||||
|
||||
# Determine if a webapp is defined in the manifest.
|
||||
HAS_WEBAPP ?= $(shell build/bin/manifest has_webapp)
|
||||
|
||||
# Determine if a /public folder is in use
|
||||
HAS_PUBLIC ?= $(wildcard public/.)
|
||||
|
||||
# Determine if the mattermost-utilities repo is present
|
||||
HAS_MM_UTILITIES ?= $(wildcard $(MM_UTILITIES_DIR)/.)
|
||||
|
||||
# Store the current path for later use
|
||||
PWD ?= $(shell pwd)
|
||||
|
||||
# Ensure that npm (and thus node) is installed.
|
||||
ifneq ($(HAS_WEBAPP),)
|
||||
ifeq ($(NPM),)
|
||||
$(error "npm is not available: see https://www.npmjs.com/get-npm")
|
||||
endif
|
||||
endif
|
113
mattermost-plugin/build/sync/README.md
Normal file
113
mattermost-plugin/build/sync/README.md
Normal file
@ -0,0 +1,113 @@
|
||||
sync
|
||||
====
|
||||
|
||||
The sync tool is a proof-of-concept implementation of a tool for synchronizing mattermost plugin
|
||||
repositories with the mattermost-plugin-starter-template repo.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
At its core the tool is just a collection of checks and actions that are executed according to a
|
||||
synchronization plan (see [./build/sync/plan.yml](https://github.com/mattermost/mattermost-plugin-starter-template/blob/sync/build/sync/plan.yml)
|
||||
for an example). The plan defines a set of files
|
||||
and/or directories that need to be kept in sync between the plugin repository and the template (this
|
||||
repo).
|
||||
|
||||
For each set of paths, a set of actions to be performed is outlined. No more than one action of that set
|
||||
will be executed - the first one whose checks pass. Other actions are meant to act as fallbacks.
|
||||
The idea is to be able to e.g. overwrite a file if it has no local changes or apply a format-specific
|
||||
merge algorithm otherwise.
|
||||
|
||||
Before running each action, the tool will check if any checks are defined for that action. If there
|
||||
are any, they will be executed and their results examined. If all checks pass, the action will be executed.
|
||||
If there is a check failure, the tool will locate the next applicable action according to the plan and
|
||||
start over with it.
|
||||
|
||||
The synchronization plan can also run checks before running any actions, e.g. to check if the source and
|
||||
target worktrees are clean.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The tool can be executed from the root of this repository with a command:
|
||||
```
|
||||
$ go run ./build/sync/main.go ./build/sync/plan.yml ../mattermost-plugin-github
|
||||
```
|
||||
|
||||
(assuming `mattermost-plugin-github` is the target repository we want to synchronize with the source).
|
||||
|
||||
plan.yml
|
||||
---------
|
||||
|
||||
The `plan.yml` file (located in `build/sync/plan.yml`) consists of two parts:
|
||||
- checks
|
||||
- actions
|
||||
|
||||
The `checks` section defines tests to run before executing the plan itself. Currently the only available such check is `repo_is_clean` defined as:
|
||||
```
|
||||
type: repo_is_clean
|
||||
params:
|
||||
repo: source
|
||||
```
|
||||
The `repo` parameter takes one of two values:
|
||||
- `source` - the `mattermost-plugin-starter-template` repository
|
||||
- `target` - the repository of the plugin being updated.
|
||||
|
||||
The `actions` section defines actions to be run as part of the synchronization.
|
||||
Each entry in this section has the form:
|
||||
```
|
||||
paths:
|
||||
- path1
|
||||
- path2
|
||||
actions:
|
||||
- type: action_type
|
||||
params:
|
||||
action_parameter: value
|
||||
conditions:
|
||||
- type: check_type
|
||||
params:
|
||||
check_parameter: value
|
||||
```
|
||||
|
||||
`paths` is a list of file or directory paths (relative to the root of the repository)
|
||||
synchronization should be performed on.
|
||||
|
||||
Each action in the `actions` section is defined by its type. Currently supported action types are:
|
||||
- `overwrite_file` - overwrite the specified file in the `target` repository with the file in the `source` repository.
|
||||
- `overwrite_directory` - overwrite a directory.
|
||||
|
||||
Both actions accept a parameter called `create` which determines if the file or directory should be created if it does not exist in the target repository.
|
||||
|
||||
The `conditions` part of an action definition defines tests that need to pass for the
|
||||
action to be run. Available checks are:
|
||||
- `exists`
|
||||
- `file_unaltered`
|
||||
|
||||
The `exists` check takes a single parameter - `repo` (referencing either the source or target repository) and it passes only if the file or directory the action is about to be run on exists. If the repo parameter is not specified, it will default to `target`.
|
||||
|
||||
The `file_unaltered` check is only applicable to file paths. It passes if the file
|
||||
has not been altered - i.e. it is identical to some version of that same file in the reference repository (usually `source`). This check takes two parameters:
|
||||
- `in` - repository to check the file in, default `target`
|
||||
- `compared-to` - repository to check the file against, default `source`.
|
||||
|
||||
When multiple actions are specified for a set of paths, the `sync` tool will only
|
||||
execute a single action for each path. The first action in the list, whose conditions
|
||||
are all satisfied will be executed.
|
||||
|
||||
If an acton fails due to an error, the synchronization run will be aborted.
|
||||
|
||||
Caveat emptor
|
||||
-------------
|
||||
|
||||
This is a very basic proof-of-concept and there are many things that should be improved/implemented:
|
||||
(in no specific order)
|
||||
|
||||
1. Format-specific merge actions for `go.mod`, `go.sum`, `webapp/package.json` and other files should
|
||||
be implemented.
|
||||
2. Better logging should be implemented.
|
||||
3. Handling action dependencies should be investigated.
|
||||
e.g. if the `build` directory is overwritten, that will in some cases mean that the go.mod file also needs
|
||||
to be updated.
|
||||
4. Storing the tree-hash of the template repository that the plugin was synchronized with would allow
|
||||
improving the performance of the tool by restricting the search space when examining if a file
|
||||
has been altered in the plugin repository.
|
85
mattermost-plugin/build/sync/main.go
Normal file
85
mattermost-plugin/build/sync/main.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("verbose", false, "enable verbose output")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Update a pluging directory with /mattermost-plugin-starter-template/.\n")
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "%s <plan.yml> <plugin_directory>\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
// TODO: implement proper command line parameter parsing.
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Fprintf(os.Stderr, "running: \n $ sync [plan.yaml] [plugin path]\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
syncPlan, err := readPlan(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "coud not load plan: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srcDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to get current directory: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
trgDir, err := filepath.Abs(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not determine target directory: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srcRepo, err := plan.GetRepoSetup(srcDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
trgRepo, err := plan.GetRepoSetup(trgDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
planSetup := plan.Setup{
|
||||
Source: srcRepo,
|
||||
Target: trgRepo,
|
||||
VerboseLogging: *verbose,
|
||||
}
|
||||
err = syncPlan.Execute(planSetup)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func readPlan(path string) (*plan.Plan, error) {
|
||||
raw, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plan file %q: %v", path, err)
|
||||
}
|
||||
|
||||
var p plan.Plan
|
||||
err = yaml.Unmarshal(raw, &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal plan yaml: %v", err)
|
||||
}
|
||||
|
||||
return &p, err
|
||||
}
|
44
mattermost-plugin/build/sync/plan.yml
Normal file
44
mattermost-plugin/build/sync/plan.yml
Normal file
@ -0,0 +1,44 @@
|
||||
checks:
|
||||
- type: repo_is_clean
|
||||
params:
|
||||
repo: source
|
||||
- type: repo_is_clean
|
||||
params:
|
||||
repo: target
|
||||
actions:
|
||||
- paths:
|
||||
- build/pluginctl
|
||||
- build/manifest
|
||||
actions:
|
||||
- type: overwrite_directory
|
||||
params:
|
||||
create: true
|
||||
- paths:
|
||||
- Makefile
|
||||
actions:
|
||||
- type: overwrite_file
|
||||
params:
|
||||
create: true
|
||||
- paths:
|
||||
- .editorconfig
|
||||
- .gitattributes
|
||||
- .gitignore
|
||||
- build/.gitignore
|
||||
- build/go.mod
|
||||
- build/go.sum
|
||||
- build/setup.mk
|
||||
- server/.gitignore
|
||||
- webapp/.eslintrc.json
|
||||
- webapp/.npmrc
|
||||
- webapp/babel.config.js
|
||||
- webapp/package.json
|
||||
- webapp/tsconfig.json
|
||||
- webapp/webpack.config.js
|
||||
- webapp/src/manifest.test.tsx
|
||||
- webapp/tests/setup.tsx
|
||||
actions:
|
||||
- type: overwrite_file
|
||||
params:
|
||||
create: true
|
||||
conditions:
|
||||
- type: file_unaltered
|
214
mattermost-plugin/build/sync/plan/actions.go
Normal file
214
mattermost-plugin/build/sync/plan/actions.go
Normal file
@ -0,0 +1,214 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ActionConditions adds condition support to actions.
|
||||
type ActionConditions struct {
|
||||
// Conditions are checkers run before executing the
|
||||
// action. If any one fails (returns an error), the action
|
||||
// itself is not executed.
|
||||
Conditions []Check
|
||||
}
|
||||
|
||||
// Check runs the conditions associated with the action and returns
|
||||
// the first error (if any).
|
||||
func (c ActionConditions) Check(path string, setup Setup) error {
|
||||
if len(c.Conditions) > 0 {
|
||||
setup.Logf("checking action conditions")
|
||||
}
|
||||
for _, condition := range c.Conditions {
|
||||
err := condition.Check(path, setup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverwriteFileAction is used to overwrite a file.
|
||||
type OverwriteFileAction struct {
|
||||
ActionConditions
|
||||
Params struct {
|
||||
// Create determines whether the target directory
|
||||
// will be created if it does not exist.
|
||||
Create bool `json:"create"`
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements plan.Action.Run.
|
||||
func (a OverwriteFileAction) Run(path string, setup Setup) error {
|
||||
setup.Logf("overwriting file %q", path)
|
||||
src := setup.PathInRepo(SourceRepo, path)
|
||||
dst := setup.PathInRepo(TargetRepo, path)
|
||||
|
||||
dstInfo, err := os.Stat(dst)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
if !a.Params.Create {
|
||||
return fmt.Errorf("path %q does not exist, not creating", dst)
|
||||
}
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to check path %q: %v", dst, err)
|
||||
case dstInfo.IsDir():
|
||||
return fmt.Errorf("path %q is a directory", dst)
|
||||
}
|
||||
|
||||
srcInfo, err := os.Stat(src)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("file %q does not exist", src)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to check path %q: %v", src, err)
|
||||
}
|
||||
if srcInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is a directory", src)
|
||||
}
|
||||
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q: %v", src, err)
|
||||
}
|
||||
defer srcF.Close()
|
||||
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcInfo.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q: %v", src, err)
|
||||
}
|
||||
defer dstF.Close()
|
||||
_, err = io.Copy(dstF, srcF)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file %q: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverwriteDirectoryAction is used to completely overwrite directories.
|
||||
// If the target directory exists, it will be removed first.
|
||||
type OverwriteDirectoryAction struct {
|
||||
ActionConditions
|
||||
Params struct {
|
||||
// Create determines whether the target directory
|
||||
// will be created if it does not exist.
|
||||
Create bool `json:"create"`
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements plan.Action.Run.
|
||||
func (a OverwriteDirectoryAction) Run(path string, setup Setup) error {
|
||||
setup.Logf("overwriting directory %q", path)
|
||||
src := setup.PathInRepo(SourceRepo, path)
|
||||
dst := setup.PathInRepo(TargetRepo, path)
|
||||
|
||||
dstInfo, err := os.Stat(dst)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
if !a.Params.Create {
|
||||
return fmt.Errorf("path %q does not exist, not creating", dst)
|
||||
}
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to check path %q: %v", dst, err)
|
||||
default:
|
||||
if !dstInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is not a directory", dst)
|
||||
}
|
||||
err = os.RemoveAll(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove directory %q: %v", dst, err)
|
||||
}
|
||||
}
|
||||
|
||||
srcInfo, err := os.Stat(src)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", src)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to check path %q: %v", src, err)
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf("path %q is not a directory", src)
|
||||
}
|
||||
|
||||
err = CopyDirectory(src, dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy path %q: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDirectory copies the directory src to dst so that after
|
||||
// a successful operation the contents of src and dst are equal.
|
||||
func CopyDirectory(src, dst string) error {
|
||||
copier := dirCopier{dst: dst, src: src}
|
||||
return filepath.Walk(src, copier.Copy)
|
||||
}
|
||||
|
||||
type dirCopier struct {
|
||||
dst string
|
||||
src string
|
||||
}
|
||||
|
||||
// Convert a path in the source directory to a path in the destination
|
||||
// directory.
|
||||
func (d dirCopier) srcToDst(path string) (string, error) {
|
||||
suff := strings.TrimPrefix(path, d.src)
|
||||
if suff == path {
|
||||
return "", fmt.Errorf("path %q is not in %q", path, d.src)
|
||||
}
|
||||
return filepath.Join(d.dst, suff), nil
|
||||
}
|
||||
|
||||
// Copy is an implementation of filepatch.WalkFunc that copies the
|
||||
// source directory to target with all subdirectories.
|
||||
func (d dirCopier) Copy(srcPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy directory: %v", err)
|
||||
}
|
||||
trgPath, err := d.srcToDst(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
err = os.MkdirAll(trgPath, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
|
||||
}
|
||||
err = os.Chtimes(trgPath, info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory %q: %v", trgPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = copyFile(srcPath, trgPath, info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file %q: %v", srcPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, info os.FileInfo) error {
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file %q: %v", src, err)
|
||||
}
|
||||
defer srcF.Close()
|
||||
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open destination file %q: %v", dst, err)
|
||||
}
|
||||
_, err = io.Copy(dstF, srcF)
|
||||
if err != nil {
|
||||
dstF.Close()
|
||||
return fmt.Errorf("failed to copy file %q: %v", src, err)
|
||||
}
|
||||
if err = dstF.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close file %q: %v", dst, err)
|
||||
}
|
||||
err = os.Chtimes(dst, info.ModTime(), info.ModTime())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to adjust file modification time for %q: %v", dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
112
mattermost-plugin/build/sync/plan/actions_test.go
Normal file
112
mattermost-plugin/build/sync/plan/actions_test.go
Normal file
@ -0,0 +1,112 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func TestCopyDirectory(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
srcDir := filepath.Join(wd, "testdata")
|
||||
err = plan.CopyDirectory(srcDir, dir)
|
||||
assert.Nil(err)
|
||||
|
||||
compareDirectories(t, dir, srcDir)
|
||||
}
|
||||
|
||||
func TestOverwriteFileAction(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
setup := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: filepath.Join(wd, "testdata", "b"),
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: dir,
|
||||
},
|
||||
}
|
||||
action := plan.OverwriteFileAction{}
|
||||
action.Params.Create = true
|
||||
err = action.Run("c", setup)
|
||||
assert.Nil(err)
|
||||
|
||||
compareDirectories(t, dir, filepath.Join(wd, "testdata", "b"))
|
||||
}
|
||||
|
||||
func TestOverwriteDirectoryAction(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a temporary directory to copy to.
|
||||
dir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
assert.Nil(err)
|
||||
|
||||
setup := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: wd,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Git: nil,
|
||||
Path: dir,
|
||||
},
|
||||
}
|
||||
action := plan.OverwriteDirectoryAction{}
|
||||
action.Params.Create = true
|
||||
err = action.Run("testdata", setup)
|
||||
assert.Nil(err)
|
||||
|
||||
destDir := filepath.Join(dir, "testdata")
|
||||
srcDir := filepath.Join(wd, "testdata")
|
||||
compareDirectories(t, destDir, srcDir)
|
||||
}
|
||||
|
||||
func compareDirectories(t *testing.T, pathA, pathB string) {
|
||||
assert := assert.New(t)
|
||||
t.Helper()
|
||||
|
||||
aContents, err := ioutil.ReadDir(pathA)
|
||||
assert.Nil(err)
|
||||
bContents, err := ioutil.ReadDir(pathB)
|
||||
assert.Nil(err)
|
||||
assert.Len(aContents, len(bContents))
|
||||
|
||||
// Check the directory contents are equal.
|
||||
for i, aFInfo := range aContents {
|
||||
bFInfo := bContents[i]
|
||||
assert.Equal(aFInfo.Name(), bFInfo.Name())
|
||||
assert.Equal(aFInfo.Mode(), bFInfo.Mode())
|
||||
assert.Equal(aFInfo.IsDir(), bFInfo.IsDir())
|
||||
if !aFInfo.IsDir() {
|
||||
assert.Equal(aFInfo.Size(), bFInfo.Size())
|
||||
}
|
||||
}
|
||||
}
|
176
mattermost-plugin/build/sync/plan/checks.go
Normal file
176
mattermost-plugin/build/sync/plan/checks.go
Normal file
@ -0,0 +1,176 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
|
||||
)
|
||||
|
||||
// CheckFail is a custom error type used to indicate a
|
||||
// check that did not pass (but did not fail due to external
|
||||
// causes.
|
||||
// Use `IsCheckFail` to check if an error is a check failure.
|
||||
type CheckFail string
|
||||
|
||||
func (e CheckFail) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// CheckFailf creates an error with the specified message string.
|
||||
// The error will pass the IsCheckFail filter.
|
||||
func CheckFailf(msg string, args ...interface{}) CheckFail {
|
||||
if len(args) > 0 {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
return CheckFail(msg)
|
||||
}
|
||||
|
||||
// IsCheckFail determines if an error is a check fail error.
|
||||
func IsCheckFail(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := err.(CheckFail)
|
||||
return ok
|
||||
}
|
||||
|
||||
// RepoIsCleanChecker checks whether the git repository is clean.
|
||||
type RepoIsCleanChecker struct {
|
||||
Params struct {
|
||||
Repo RepoID
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
// The path parameter is ignored because this checker checks the state of a repository.
|
||||
func (r RepoIsCleanChecker) Check(_ string, ctx Setup) error {
|
||||
ctx.Logf("checking if repository %q is clean", r.Params.Repo)
|
||||
rc := ctx.GetRepo(r.Params.Repo)
|
||||
repo := rc.Git
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree: %v", err)
|
||||
}
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree status: %v", err)
|
||||
}
|
||||
if !status.IsClean() {
|
||||
return CheckFailf("%q repository is not clean", r.Params.Repo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PathExistsChecker checks whether the fle or directory with the
|
||||
// path exists. If it does not, an error is returned.
|
||||
type PathExistsChecker struct {
|
||||
Params struct {
|
||||
Repo RepoID
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
func (r PathExistsChecker) Check(path string, ctx Setup) error {
|
||||
repo := r.Params.Repo
|
||||
if repo == "" {
|
||||
repo = TargetRepo
|
||||
}
|
||||
ctx.Logf("checking if path %q exists in repo %q", path, repo)
|
||||
absPath := ctx.PathInRepo(repo, path)
|
||||
_, err := os.Stat(absPath)
|
||||
if os.IsNotExist(err) {
|
||||
return CheckFailf("path %q does not exist", path)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to stat path %q: %v", absPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileUnalteredChecker checks whether the file in Repo is
|
||||
// an unaltered version of that same file in ReferenceRepo.
|
||||
//
|
||||
// Its purpose is to check that a file has not been changed after forking a repository.
|
||||
// It could be an old unaltered version, so the git history of the file is traversed
|
||||
// until a matching version is found.
|
||||
//
|
||||
// If the repositories in the parameters are not specified,
|
||||
// reference will default to the source repository and repo - to the target.
|
||||
type FileUnalteredChecker struct {
|
||||
Params struct {
|
||||
SourceRepo RepoID `json:"compared-to"`
|
||||
TargetRepo RepoID `json:"in"`
|
||||
}
|
||||
}
|
||||
|
||||
// Check implements the Checker interface.
|
||||
func (f FileUnalteredChecker) Check(path string, setup Setup) error {
|
||||
setup.Logf("checking if file %q has not been altered", path)
|
||||
repo := f.Params.TargetRepo
|
||||
if repo == "" {
|
||||
repo = TargetRepo
|
||||
}
|
||||
source := f.Params.SourceRepo
|
||||
if source == "" {
|
||||
source = SourceRepo
|
||||
}
|
||||
trgPath := setup.PathInRepo(repo, path)
|
||||
srcPath := setup.PathInRepo(source, path)
|
||||
|
||||
fileHashes, err := git.FileHistory(path, setup.GetRepo(source).Git)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var srcDeleted bool
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
srcDeleted = true
|
||||
} else {
|
||||
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
|
||||
}
|
||||
} else if srcInfo.IsDir() {
|
||||
return fmt.Errorf("%q is a directory in source repository", path)
|
||||
}
|
||||
|
||||
trgInfo, err := os.Stat(trgPath)
|
||||
if os.IsNotExist(err) {
|
||||
if srcDeleted {
|
||||
// File has been deleted in target and source repositories.
|
||||
// Consider it unaltered.
|
||||
return nil
|
||||
}
|
||||
// Check if the file was ever in git history.
|
||||
_, err := git.FileHistory(path, setup.GetRepo(repo).Git)
|
||||
if errors.Is(err, git.ErrNotFound) {
|
||||
// This is a new file being introduced to the target repo.
|
||||
// Consider it unaltered.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return CheckFailf("file %q has been deleted", trgPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stat for %q: %v", trgPath, err)
|
||||
}
|
||||
if trgInfo.IsDir() {
|
||||
return fmt.Errorf("%q is a directory", trgPath)
|
||||
}
|
||||
|
||||
currentHash, err := git.GetFileHash(trgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(fileHashes)
|
||||
idx := sort.SearchStrings(fileHashes, currentHash)
|
||||
if idx < len(fileHashes) && fileHashes[idx] == currentHash {
|
||||
return nil
|
||||
}
|
||||
return CheckFailf("file %q has been altered", trgPath)
|
||||
}
|
213
mattermost-plugin/build/sync/plan/checks_test.go
Normal file
213
mattermost-plugin/build/sync/plan/checks_test.go
Normal file
@ -0,0 +1,213 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
// Tests for the RepoIsClean checker.
|
||||
func TestRepoIsCleanChecker(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create a git repository in a temporary dir.
|
||||
dir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
repo, err := git.PlainInit(dir, false)
|
||||
assert.Nil(err)
|
||||
|
||||
// Repo should be clean.
|
||||
checker := plan.RepoIsCleanChecker{}
|
||||
checker.Params.Repo = plan.TargetRepo
|
||||
|
||||
ctx := plan.Setup{
|
||||
Target: plan.RepoSetup{
|
||||
Path: dir,
|
||||
Git: repo,
|
||||
},
|
||||
}
|
||||
assert.Nil(checker.Check("", ctx))
|
||||
|
||||
// Create a file in the repository.
|
||||
err = ioutil.WriteFile(path.Join(dir, "data.txt"), []byte("lorem ipsum"), 0600)
|
||||
assert.Nil(err)
|
||||
err = checker.Check("", ctx)
|
||||
assert.EqualError(err, "\"target\" repository is not clean")
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
}
|
||||
|
||||
func TestPathExistsChecker(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Set up a working directory.
|
||||
wd, err := ioutil.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(wd)
|
||||
err = os.Mkdir(filepath.Join(wd, "t"), 0755)
|
||||
assert.Nil(err)
|
||||
err = ioutil.WriteFile(filepath.Join(wd, "t", "test"), []byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
|
||||
checker := plan.PathExistsChecker{}
|
||||
checker.Params.Repo = plan.SourceRepo
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
// Check with existing directory.
|
||||
assert.Nil(checker.Check("t", ctx))
|
||||
|
||||
// Check with existing file.
|
||||
assert.Nil(checker.Check("t/test", ctx))
|
||||
|
||||
err = checker.Check("nosuchpath", ctx)
|
||||
assert.NotNil(err)
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
}
|
||||
|
||||
func tempGitRepo(assert *assert.Assertions) (string, *git.Repository, func()) {
|
||||
// Setup repository.
|
||||
wd, err := ioutil.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
|
||||
// Initialize a repository.
|
||||
repo, err := git.PlainInit(wd, false)
|
||||
assert.Nil(err)
|
||||
w, err := repo.Worktree()
|
||||
assert.Nil(err)
|
||||
// Create repository files.
|
||||
err = ioutil.WriteFile(filepath.Join(wd, "test"),
|
||||
[]byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
sig := &object.Signature{
|
||||
Name: "test",
|
||||
Email: "test@example.com",
|
||||
When: time.Now(),
|
||||
}
|
||||
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
pathA := "a.txt"
|
||||
err = ioutil.WriteFile(filepath.Join(wd, pathA),
|
||||
[]byte("lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathA)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
|
||||
return wd, repo, func() { os.RemoveAll(wd) }
|
||||
|
||||
}
|
||||
|
||||
func TestUnalteredCheckerSameFile(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
// Check with the same file - check should succeed
|
||||
hashPath := "a.txt"
|
||||
err := checker.Check(hashPath, ctx)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
func TestUnalteredCheckerDifferentContents(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: wd,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
// Create a file with the same suffix path, but different contents.
|
||||
tmpDir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "a.txt"),
|
||||
[]byte("not lorem ipsum"), 0644)
|
||||
assert.Nil(err)
|
||||
|
||||
// Set the plugin path to the temporary directory.
|
||||
ctx.Target.Path = tmpDir
|
||||
err = checker.Check("a.txt", ctx)
|
||||
assert.True(plan.IsCheckFail(err))
|
||||
assert.EqualError(err, fmt.Sprintf("file %q has been altered", filepath.Join(tmpDir, "a.txt")))
|
||||
|
||||
}
|
||||
|
||||
// TestUnalteredCheckerNonExistant tests running the unaltered file checker
|
||||
// in the case where the target file does not exist. If the files has no history,
|
||||
// the checker should pass.
|
||||
func TestUnalteredCheckerNonExistant(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
hashPath := "a.txt"
|
||||
|
||||
wd, repo, cleanup := tempGitRepo(assert)
|
||||
defer cleanup()
|
||||
|
||||
// Temporary repo.
|
||||
tmpDir, err := ioutil.TempDir("", "test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
trgRepo, err := git.PlainInit(tmpDir, false)
|
||||
assert.Nil(err)
|
||||
|
||||
ctx := plan.Setup{
|
||||
Source: plan.RepoSetup{
|
||||
Path: wd,
|
||||
Git: repo,
|
||||
},
|
||||
Target: plan.RepoSetup{
|
||||
Path: tmpDir,
|
||||
Git: trgRepo,
|
||||
},
|
||||
}
|
||||
|
||||
checker := plan.FileUnalteredChecker{}
|
||||
checker.Params.SourceRepo = plan.SourceRepo
|
||||
checker.Params.TargetRepo = plan.TargetRepo
|
||||
|
||||
err = checker.Check(hashPath, ctx)
|
||||
assert.Nil(err)
|
||||
}
|
111
mattermost-plugin/build/sync/plan/git/file_history.go
Normal file
111
mattermost-plugin/build/sync/plan/git/file_history.go
Normal file
@ -0,0 +1,111 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha1" //nolint
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotFound signifies the file was not found.
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
|
||||
// FileHistory will trace all the versions of a file in the git repository
|
||||
// and return a list of sha1 hashes of that file.
|
||||
func FileHistory(path string, repo *git.Repository) ([]string, error) {
|
||||
logOpts := git.LogOptions{
|
||||
FileName: &path,
|
||||
All: true,
|
||||
}
|
||||
commits, err := repo.Log(&logOpts)
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commits for path %q: %v", path, err)
|
||||
}
|
||||
defer commits.Close()
|
||||
hashHistory := []string{}
|
||||
cerr := commits.ForEach(func(c *object.Commit) error {
|
||||
root, err := repo.TreeObject(c.TreeHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get commit tree: %v", err)
|
||||
}
|
||||
f, err := traverseTree(root, path)
|
||||
if err == object.ErrFileNotFound || err == object.ErrDirectoryNotFound {
|
||||
// Ignoring file not found errors.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
sum, err := getReaderHash(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashHistory = append(hashHistory, sum)
|
||||
return nil
|
||||
})
|
||||
if cerr != nil && cerr != io.EOF {
|
||||
return nil, cerr
|
||||
}
|
||||
if len(hashHistory) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return hashHistory, nil
|
||||
}
|
||||
|
||||
func traverseTree(root *object.Tree, path string) (io.ReadCloser, error) {
|
||||
dirName, fileName := filepath.Split(path)
|
||||
var err error
|
||||
t := root
|
||||
if dirName != "" {
|
||||
t, err = root.Tree(filepath.Clean(dirName))
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to traverse tree to %q: %v", dirName, err)
|
||||
}
|
||||
}
|
||||
f, err := t.File(fileName)
|
||||
if err == object.ErrFileNotFound {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup file %q: %v", fileName, err)
|
||||
}
|
||||
reader, err := f.Reader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open %q: %v", path, err)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func getReaderHash(r io.Reader) (string, error) {
|
||||
h := sha1.New() // nolint
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// GetFileHash calculates the sha1 hash sum of the file.
|
||||
func GetFileHash(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
sum, err := getReaderHash(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sum, nil
|
||||
}
|
80
mattermost-plugin/build/sync/plan/git/file_history_test.go
Normal file
80
mattermost-plugin/build/sync/plan/git/file_history_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
gitutil "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git"
|
||||
)
|
||||
|
||||
var fileContents = []byte("abcdefg")
|
||||
|
||||
func TestFileHistory(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
dir, err := ioutil.TempDir("", "repo")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Initialize a repository.
|
||||
repo, err := git.PlainInit(dir, false)
|
||||
assert.Nil(err)
|
||||
w, err := repo.Worktree()
|
||||
assert.Nil(err)
|
||||
// Create repository files.
|
||||
err = ioutil.WriteFile(filepath.Join(dir, "test"), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add("test")
|
||||
assert.Nil(err)
|
||||
sig := &object.Signature{
|
||||
Name: "test",
|
||||
Email: "test@example.com",
|
||||
When: time.Now(),
|
||||
}
|
||||
_, err = w.Commit("initial commit", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
pathA := "a.txt"
|
||||
err = ioutil.WriteFile(filepath.Join(dir, pathA), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
pathB := "b.txt"
|
||||
err = ioutil.WriteFile(filepath.Join(dir, pathB), fileContents, 0644)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathA)
|
||||
assert.Nil(err)
|
||||
_, err = w.Add(pathB)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("add files", &git.CommitOptions{Author: sig})
|
||||
assert.Nil(err)
|
||||
// Delete one of the files.
|
||||
_, err = w.Remove(pathB)
|
||||
assert.Nil(err)
|
||||
_, err = w.Commit("remove file b.txt", &git.CommitOptions{
|
||||
Author: sig,
|
||||
All: true,
|
||||
})
|
||||
assert.Nil(err)
|
||||
|
||||
repo, err = git.PlainOpen(dir)
|
||||
assert.Nil(err)
|
||||
|
||||
// Call file history on an existing file.
|
||||
sums, err := gitutil.FileHistory("a.txt", repo)
|
||||
assert.Nil(err)
|
||||
assert.Equal([]string{"2fb5e13419fc89246865e7a324f476ec624e8740"}, sums)
|
||||
|
||||
// Calling with a non-existent file returns error.
|
||||
sums, err = gitutil.FileHistory(filepath.Join(dir, "nosuch_testfile.txt"), repo)
|
||||
assert.Equal(gitutil.ErrNotFound, err)
|
||||
assert.Nil(sums)
|
||||
|
||||
// Calling with a non-existent file that was in git history returns no error.
|
||||
_, err = gitutil.FileHistory(pathB, repo)
|
||||
assert.Nil(err)
|
||||
}
|
245
mattermost-plugin/build/sync/plan/plan.go
Normal file
245
mattermost-plugin/build/sync/plan/plan.go
Normal file
@ -0,0 +1,245 @@
|
||||
// Package plan handles the synchronization plan.
|
||||
//
|
||||
// Each synchronization plan is a set of checks and actions to perform on specified paths
|
||||
// that will result in the "plugin" repository being updated.
|
||||
package plan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Plan defines the plan for synchronizing a target and a source directory.
|
||||
type Plan struct {
|
||||
Checks []Check `json:"checks"`
|
||||
// Each set of paths has multiple actions associated, each a fallback for the one
|
||||
// previous to it.
|
||||
Actions []ActionSet
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the `json.Unmarshaler` interface.
|
||||
func (p *Plan) UnmarshalJSON(raw []byte) error {
|
||||
var t jsonPlan
|
||||
if err := json.Unmarshal(raw, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Checks = make([]Check, len(t.Checks))
|
||||
for i, check := range t.Checks {
|
||||
c, err := parseCheck(check.Type, check.Params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse check %q: %v", check.Type, err)
|
||||
}
|
||||
p.Checks[i] = c
|
||||
}
|
||||
|
||||
if len(t.Actions) > 0 {
|
||||
p.Actions = make([]ActionSet, len(t.Actions))
|
||||
}
|
||||
for i, actionSet := range t.Actions {
|
||||
var err error
|
||||
pathActions := make([]Action, len(actionSet.Actions))
|
||||
for i, action := range actionSet.Actions {
|
||||
var actionConditions []Check
|
||||
if len(action.Conditions) > 0 {
|
||||
actionConditions = make([]Check, len(action.Conditions))
|
||||
}
|
||||
for j, check := range action.Conditions {
|
||||
actionConditions[j], err = parseCheck(check.Type, check.Params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pathActions[i], err = parseAction(action.Type, action.Params, actionConditions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.Actions[i] = ActionSet{
|
||||
Paths: actionSet.Paths,
|
||||
Actions: pathActions,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute executes the synchronization plan.
|
||||
func (p *Plan) Execute(c Setup) error {
|
||||
c.Logf("running pre-checks")
|
||||
for _, check := range p.Checks {
|
||||
err := check.Check("", c) // For pre-sync checks, the path is ignored.
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed check: %v", err)
|
||||
}
|
||||
}
|
||||
result := []pathResult{}
|
||||
c.Logf("running actions")
|
||||
for _, actions := range p.Actions {
|
||||
PATHS_LOOP:
|
||||
for _, path := range actions.Paths {
|
||||
c.Logf("syncing path %q", path)
|
||||
ACTIONS_LOOP:
|
||||
for i, action := range actions.Actions {
|
||||
c.Logf("running action for path %q", path)
|
||||
err := action.Check(path, c)
|
||||
if IsCheckFail(err) {
|
||||
c.Logf("check failed, not running action: %v", err)
|
||||
// If a check for an action fails, we switch to
|
||||
// the next action associated with the path.
|
||||
if i == len(actions.Actions)-1 { // no actions to fallback to.
|
||||
c.Logf("path %q not handled - no more fallbacks", path)
|
||||
result = append(result,
|
||||
pathResult{
|
||||
Path: path,
|
||||
Status: statusFailed,
|
||||
Message: fmt.Sprintf("check failed, %s", err.Error()),
|
||||
})
|
||||
}
|
||||
continue ACTIONS_LOOP
|
||||
} else if err != nil {
|
||||
c.LogErrorf("unexpected error when running check: %v", err)
|
||||
return fmt.Errorf("failed to run checks for action: %v", err)
|
||||
}
|
||||
err = action.Run(path, c)
|
||||
if err != nil {
|
||||
c.LogErrorf("action failed: %v", err)
|
||||
return fmt.Errorf("action failed: %v", err)
|
||||
}
|
||||
c.Logf("path %q sync'ed successfully", path)
|
||||
result = append(result,
|
||||
pathResult{
|
||||
Path: path,
|
||||
Status: statusUpdated,
|
||||
})
|
||||
|
||||
continue PATHS_LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print execution result.
|
||||
sort.SliceStable(result, func(i, j int) bool { return result[i].Path < result[j].Path })
|
||||
for _, res := range result {
|
||||
if res.Message != "" {
|
||||
fmt.Fprintf(os.Stdout, "%s\t%s: %s\n", res.Status, res.Path, res.Message)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "%s\t%s\n", res.Status, res.Path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check returns an error if the condition fails.
|
||||
type Check interface {
|
||||
Check(string, Setup) error
|
||||
}
|
||||
|
||||
// ActionSet is a set of actions along with a set of paths to
|
||||
// perform those actions on.
|
||||
type ActionSet struct {
|
||||
Paths []string
|
||||
Actions []Action
|
||||
}
|
||||
|
||||
// Action runs the defined action.
|
||||
type Action interface {
|
||||
// Run performs the action on the specified path.
|
||||
Run(string, Setup) error
|
||||
// Check runs checks associated with the action
|
||||
// before running it.
|
||||
Check(string, Setup) error
|
||||
}
|
||||
|
||||
// jsonPlan is used to unmarshal Plan structures.
|
||||
type jsonPlan struct {
|
||||
Checks []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
Actions []struct {
|
||||
Paths []string `json:"paths"`
|
||||
Actions []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Conditions []struct {
|
||||
Type string `json:"type"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheck(checkType string, rawParams json.RawMessage) (Check, error) {
|
||||
var c Check
|
||||
|
||||
var params interface{}
|
||||
|
||||
switch checkType {
|
||||
case "repo_is_clean":
|
||||
tc := RepoIsCleanChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
case "exists":
|
||||
tc := PathExistsChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
case "file_unaltered":
|
||||
tc := FileUnalteredChecker{}
|
||||
params = &tc.Params
|
||||
c = &tc
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown checker type %q", checkType)
|
||||
}
|
||||
|
||||
if len(rawParams) > 0 {
|
||||
err := json.Unmarshal(rawParams, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", checkType, err)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func parseAction(actionType string, rawParams json.RawMessage, checks []Check) (Action, error) {
|
||||
var a Action
|
||||
|
||||
var params interface{}
|
||||
|
||||
switch actionType {
|
||||
case "overwrite_file":
|
||||
ta := OverwriteFileAction{}
|
||||
ta.Conditions = checks
|
||||
params = &ta.Params
|
||||
a = &ta
|
||||
case "overwrite_directory":
|
||||
ta := OverwriteDirectoryAction{}
|
||||
ta.Conditions = checks
|
||||
params = &ta.Params
|
||||
a = &ta
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown action type %q", actionType)
|
||||
}
|
||||
|
||||
if len(rawParams) > 0 {
|
||||
err := json.Unmarshal(rawParams, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal params for %s: %v", actionType, err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// pathResult contains the result of synchronizing a path.
|
||||
type pathResult struct {
|
||||
Path string
|
||||
Status status
|
||||
Message string
|
||||
}
|
||||
|
||||
type status string
|
||||
|
||||
const (
|
||||
statusUpdated status = "UPDATED"
|
||||
statusFailed status = "FAILED"
|
||||
)
|
253
mattermost-plugin/build/sync/plan/plan_test.go
Normal file
253
mattermost-plugin/build/sync/plan/plan_test.go
Normal file
@ -0,0 +1,253 @@
|
||||
package plan_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan"
|
||||
)
|
||||
|
||||
func TestUnmarshalPlan(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
rawJSON := []byte(`
|
||||
{
|
||||
"checks": [
|
||||
{"type": "repo_is_clean", "params": {"repo": "template"}}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"paths": ["abc"],
|
||||
"actions": [{
|
||||
"type": "overwrite_file",
|
||||
"params": {"create": true},
|
||||
"conditions": [{
|
||||
"type": "exists",
|
||||
"params": {"repo": "plugin"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
var p plan.Plan
|
||||
err := json.Unmarshal(rawJSON, &p)
|
||||
assert.Nil(err)
|
||||
expectedCheck := plan.RepoIsCleanChecker{}
|
||||
expectedCheck.Params.Repo = "template"
|
||||
|
||||
expectedAction := plan.OverwriteFileAction{}
|
||||
expectedAction.Params.Create = true
|
||||
expectedActionCheck := plan.PathExistsChecker{}
|
||||
expectedActionCheck.Params.Repo = "plugin"
|
||||
expectedAction.Conditions = []plan.Check{&expectedActionCheck}
|
||||
expected := plan.Plan{
|
||||
Checks: []plan.Check{&expectedCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"abc"},
|
||||
Actions: []plan.Action{
|
||||
&expectedAction,
|
||||
},
|
||||
}},
|
||||
}
|
||||
assert.Equal(expected, p)
|
||||
}
|
||||
|
||||
type mockCheck struct {
|
||||
returnErr error
|
||||
calledWith string // Path parameter the check was called with.
|
||||
}
|
||||
|
||||
// Check implements the plan.Check interface.
|
||||
func (m *mockCheck) Check(path string, c plan.Setup) error {
|
||||
m.calledWith = path
|
||||
return m.returnErr
|
||||
}
|
||||
|
||||
type mockAction struct {
|
||||
runErr error // Error to be returned by Run.
|
||||
checkErr error // Error to be returned by Check.
|
||||
calledWith string
|
||||
}
|
||||
|
||||
// Check implements plan.Action interface.
|
||||
func (m *mockAction) Check(path string, c plan.Setup) error {
|
||||
return m.checkErr
|
||||
}
|
||||
|
||||
// Run implements plan.Action interface.
|
||||
func (m *mockAction) Run(path string, c plan.Setup) error {
|
||||
m.calledWith = path
|
||||
return m.runErr
|
||||
}
|
||||
|
||||
// TestRunPlanSuccessfully tests a successful execution of a sync plan.
|
||||
func TestRunPlanSuccessfully(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
assert.Equal("somepath", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith) // second action was not called.
|
||||
}
|
||||
|
||||
// TestRunPlanPreCheckFail checks the scenario where a sync plan precheck
|
||||
// fails, aborting the whole operation.
|
||||
func TestRunPlanPreCheckFail(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{returnErr: plan.CheckFailf("check failed")}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "failed check: check failed")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
// None of the actions were executed.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanActionCheckFails tests the situation where an action's
|
||||
// check returns a recoverable error, forcing the plan to execute the fallback action.
|
||||
func TestRunPlanActionCheckFails(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
action1 := &mockAction{checkErr: plan.CheckFailf("action check failed")}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal("", action1.calledWith) // First action was not run.
|
||||
assert.Equal("somepath", action2.calledWith) // Second action was run.
|
||||
}
|
||||
|
||||
// TestRunPlanNoFallbacks tests the case where an action's check fails,
|
||||
// but there are not more fallback actions for that path.
|
||||
func TestRunPlanNoFallbacks(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
action1 := &mockAction{checkErr: plan.CheckFailf("fail")}
|
||||
action2 := &mockAction{checkErr: plan.CheckFailf("fail")}
|
||||
|
||||
p := &plan.Plan{
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.Nil(err)
|
||||
|
||||
// both actions were not executed.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanCheckError tests the scenario where a plan check fails with
|
||||
// an unexpected error. Plan execution is aborted.
|
||||
func TestRunPlanCheckError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{returnErr: fmt.Errorf("fail")}
|
||||
action1 := &mockAction{}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "failed check: fail")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
// Actions were not run.
|
||||
assert.Equal("", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith)
|
||||
}
|
||||
|
||||
// TestRunPlanActionError tests the scenario where an action fails,
|
||||
// aborting the whole sync process.
|
||||
func TestRunPlanActionError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
setup := plan.Setup{} // mocked actions and checks won't be accessing the setup.
|
||||
|
||||
preCheck := &mockCheck{}
|
||||
action1 := &mockAction{runErr: fmt.Errorf("fail")}
|
||||
action2 := &mockAction{}
|
||||
|
||||
p := &plan.Plan{
|
||||
Checks: []plan.Check{preCheck},
|
||||
Actions: []plan.ActionSet{{
|
||||
Paths: []string{"somepath"},
|
||||
Actions: []plan.Action{
|
||||
action1,
|
||||
action2,
|
||||
},
|
||||
}},
|
||||
}
|
||||
err := p.Execute(setup)
|
||||
assert.EqualError(err, "action failed: fail")
|
||||
|
||||
assert.Equal("", preCheck.calledWith)
|
||||
assert.Equal("somepath", action1.calledWith)
|
||||
assert.Equal("", action2.calledWith) // second action was not called.
|
||||
}
|
80
mattermost-plugin/build/sync/plan/setup.go
Normal file
80
mattermost-plugin/build/sync/plan/setup.go
Normal file
@ -0,0 +1,80 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// RepoID identifies a repository - either plugin or template.
|
||||
type RepoID string
|
||||
|
||||
const (
|
||||
// SourceRepo is the id of the template repository (source).
|
||||
SourceRepo RepoID = "source"
|
||||
// TargetRepo is the id of the plugin repository (target).
|
||||
TargetRepo RepoID = "target"
|
||||
)
|
||||
|
||||
// Setup contains information about both parties
|
||||
// in the sync: the plugin repository being updated
|
||||
// and the source of the update - the template repo.
|
||||
type Setup struct {
|
||||
Source RepoSetup
|
||||
Target RepoSetup
|
||||
VerboseLogging bool
|
||||
}
|
||||
|
||||
// Logf logs the provided message.
|
||||
// If verbose output is not enabled, the message will not be printed.
|
||||
func (c Setup) Logf(tpl string, args ...interface{}) {
|
||||
if c.VerboseLogging {
|
||||
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogErrorf logs the provided error message.
|
||||
func (c Setup) LogErrorf(tpl string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, tpl+"\n", args...)
|
||||
}
|
||||
|
||||
// GetRepo is a helper to get the required repo setup.
|
||||
// If the target parameter is not one of "plugin" or "template",
|
||||
// the function panics.
|
||||
func (c Setup) GetRepo(r RepoID) RepoSetup {
|
||||
switch r {
|
||||
case TargetRepo:
|
||||
return c.Target
|
||||
case SourceRepo:
|
||||
return c.Source
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot get repository setup %q", r))
|
||||
}
|
||||
}
|
||||
|
||||
// PathInRepo returns the full path of a file in the specified repository.
|
||||
func (c Setup) PathInRepo(repo RepoID, path string) string {
|
||||
r := c.GetRepo(repo)
|
||||
return filepath.Join(r.Path, path)
|
||||
}
|
||||
|
||||
// RepoSetup contains relevant information
|
||||
// about a single repository (either source or target).
|
||||
type RepoSetup struct {
|
||||
Git *git.Repository
|
||||
Path string
|
||||
}
|
||||
|
||||
// GetRepoSetup returns the repository setup for the specified path.
|
||||
func GetRepoSetup(path string) (RepoSetup, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return RepoSetup{}, fmt.Errorf("failed to access git repository at %q: %v", path, err)
|
||||
}
|
||||
return RepoSetup{
|
||||
Git: repo,
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
1
mattermost-plugin/build/sync/plan/testdata/a
vendored
Normal file
1
mattermost-plugin/build/sync/plan/testdata/a
vendored
Normal file
@ -0,0 +1 @@
|
||||
a
|
1
mattermost-plugin/build/sync/plan/testdata/b/c
vendored
Normal file
1
mattermost-plugin/build/sync/plan/testdata/b/c
vendored
Normal file
@ -0,0 +1 @@
|
||||
c
|
14
mattermost-plugin/go.mod
Normal file
14
mattermost-plugin/go.mod
Normal file
@ -0,0 +1,14 @@
|
||||
module github.com/mattermost/focalboard/mattermost-plugin
|
||||
|
||||
go 1.12
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
||||
|
||||
replace github.com/mattermost/mattermost-server/v5 => ../../mattermost-server
|
||||
|
||||
require (
|
||||
github.com/mattermost/focalboard/server v0.0.0-20210331160003-42eaa744c065
|
||||
github.com/mattermost/mattermost-server/v5 v5.34.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
1619
mattermost-plugin/go.sum
Normal file
1619
mattermost-plugin/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
26
mattermost-plugin/plugin.json
Normal file
26
mattermost-plugin/plugin.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "focalboard",
|
||||
"name": "Focalboard",
|
||||
"description": "This provides focalboard integration with mattermost.",
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-focalboard",
|
||||
"support_url": "https://github.com/mattermost/mattermost-plugin-focalboard/issues",
|
||||
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-focalboard/releases/tag/v0.1.0",
|
||||
"icon_path": "assets/starter-template-icon.svg",
|
||||
"version": "0.2.0",
|
||||
"min_server_version": "5.12.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
}
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": []
|
||||
}
|
||||
}
|
1
mattermost-plugin/public/hello.html
Normal file
1
mattermost-plugin/public/hello.html
Normal file
@ -0,0 +1 @@
|
||||
Hello from the static files public folder for the com.mattermost.plugin-starter-template plugin!
|
2
mattermost-plugin/server/.gitignore
vendored
Normal file
2
mattermost-plugin/server/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
coverage.txt
|
||||
dist
|
83
mattermost-plugin/server/configuration.go
Normal file
83
mattermost-plugin/server/configuration.go
Normal file
@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// configuration captures the plugin's external configuration as exposed in the Mattermost server
|
||||
// configuration, as well as values computed from the configuration. Any public fields will be
|
||||
// deserialized from the Mattermost server configuration in OnConfigurationChange.
|
||||
//
|
||||
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
|
||||
// configuration can change at any time, access to the configuration must be synchronized. The
|
||||
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
|
||||
// struct whenever it changes. You may replace this with whatever strategy you choose.
|
||||
//
|
||||
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
|
||||
// copy appropriate for your types.
|
||||
type configuration struct {
|
||||
}
|
||||
|
||||
// Clone shallow copies the configuration. Your implementation may require a deep copy if
|
||||
// your configuration has reference types.
|
||||
func (c *configuration) Clone() *configuration {
|
||||
var clone = *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
// getConfiguration retrieves the active configuration under lock, making it safe to use
|
||||
// concurrently. The active configuration may change underneath the client of this method, but
|
||||
// the struct returned by this API call is considered immutable.
|
||||
func (p *Plugin) getConfiguration() *configuration {
|
||||
p.configurationLock.RLock()
|
||||
defer p.configurationLock.RUnlock()
|
||||
|
||||
if p.configuration == nil {
|
||||
return &configuration{}
|
||||
}
|
||||
|
||||
return p.configuration
|
||||
}
|
||||
|
||||
// setConfiguration replaces the active configuration under lock.
|
||||
//
|
||||
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
|
||||
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
|
||||
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
|
||||
//
|
||||
// This method panics if setConfiguration is called with the existing configuration. This almost
|
||||
// certainly means that the configuration was modified without being cloned and may result in
|
||||
// an unsafe access.
|
||||
func (p *Plugin) setConfiguration(configuration *configuration) {
|
||||
p.configurationLock.Lock()
|
||||
defer p.configurationLock.Unlock()
|
||||
|
||||
if configuration != nil && p.configuration == configuration {
|
||||
// Ignore assignment if the configuration struct is empty. Go will optimize the
|
||||
// allocation for same to point at the same memory address, breaking the check
|
||||
// above.
|
||||
if reflect.ValueOf(*configuration).NumField() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
panic("setConfiguration called with the existing configuration")
|
||||
}
|
||||
|
||||
p.configuration = configuration
|
||||
}
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made.
|
||||
func (p *Plugin) OnConfigurationChange() error {
|
||||
var configuration = new(configuration)
|
||||
|
||||
// Load the public configuration fields from the Mattermost server configuration.
|
||||
if err := p.API.LoadPluginConfiguration(configuration); err != nil {
|
||||
return errors.Wrap(err, "failed to load plugin configuration")
|
||||
}
|
||||
|
||||
p.setConfiguration(configuration)
|
||||
|
||||
return nil
|
||||
}
|
9
mattermost-plugin/server/main.go
Normal file
9
mattermost-plugin/server/main.go
Normal file
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&Plugin{})
|
||||
}
|
45
mattermost-plugin/server/manifest.go
generated
Normal file
45
mattermost-plugin/server/manifest.go
generated
Normal file
@ -0,0 +1,45 @@
|
||||
// This file is automatically generated. Do not modify it manually.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
var manifest *model.Manifest
|
||||
|
||||
const manifestStr = `
|
||||
{
|
||||
"id": "focalboard",
|
||||
"name": "Focalboard",
|
||||
"description": "This provides focalboard integration with mattermost.",
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-focalboard",
|
||||
"support_url": "https://github.com/mattermost/mattermost-plugin-focalboard/issues",
|
||||
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-focalboard/releases/tag/v0.1.0",
|
||||
"icon_path": "assets/starter-template-icon.svg",
|
||||
"version": "0.2.0",
|
||||
"min_server_version": "5.12.0",
|
||||
"server": {
|
||||
"executables": {
|
||||
"linux-amd64": "server/dist/plugin-linux-amd64",
|
||||
"darwin-amd64": "server/dist/plugin-darwin-amd64",
|
||||
"windows-amd64": "server/dist/plugin-windows-amd64.exe"
|
||||
},
|
||||
"executable": ""
|
||||
},
|
||||
"webapp": {
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": []
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func init() {
|
||||
manifest = model.ManifestFromJson(strings.NewReader(manifestStr))
|
||||
}
|
129
mattermost-plugin/server/plugin.go
Normal file
129
mattermost-plugin/server/plugin.go
Normal file
@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
)
|
||||
|
||||
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
|
||||
type Plugin struct {
|
||||
plugin.MattermostPlugin
|
||||
|
||||
// configurationLock synchronizes access to the configuration.
|
||||
configurationLock sync.RWMutex
|
||||
|
||||
// configuration is the active plugin configuration. Consult getConfiguration and
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
|
||||
server *server.Server
|
||||
wsHub *WSHub
|
||||
}
|
||||
|
||||
type WSHub struct {
|
||||
API plugin.API
|
||||
handleWSMessage func(data []byte)
|
||||
}
|
||||
|
||||
func (h *WSHub) SendWSMessage(data []byte) {
|
||||
h.API.PublishPluginClusterEvent(model.PluginClusterEvent{
|
||||
Id: "websocket_event",
|
||||
Data: data,
|
||||
}, model.PluginClusterEventSendOptions{
|
||||
SendType: model.PluginClusterEventSendTypeReliable,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *WSHub) SetReceiveWSMessage(handler func(data []byte)) {
|
||||
h.handleWSMessage = handler
|
||||
}
|
||||
|
||||
func (p *Plugin) OnActivate() error {
|
||||
mmconfig := p.API.GetUnsanitizedConfig()
|
||||
filesS3Config := config.AmazonS3Config{}
|
||||
if mmconfig.FileSettings.AmazonS3AccessKeyId != nil {
|
||||
filesS3Config.AccessKeyId = *mmconfig.FileSettings.AmazonS3AccessKeyId
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SecretAccessKey != nil {
|
||||
filesS3Config.SecretAccessKey = *mmconfig.FileSettings.AmazonS3SecretAccessKey
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Bucket != nil {
|
||||
filesS3Config.Bucket = *mmconfig.FileSettings.AmazonS3Bucket
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3PathPrefix != nil {
|
||||
filesS3Config.PathPrefix = *mmconfig.FileSettings.AmazonS3PathPrefix
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Region != nil {
|
||||
filesS3Config.Region = *mmconfig.FileSettings.AmazonS3Region
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Endpoint != nil {
|
||||
filesS3Config.Endpoint = *mmconfig.FileSettings.AmazonS3Endpoint
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SSL != nil {
|
||||
filesS3Config.SSL = *mmconfig.FileSettings.AmazonS3SSL
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SignV2 != nil {
|
||||
filesS3Config.SignV2 = *mmconfig.FileSettings.AmazonS3SignV2
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3SSE != nil {
|
||||
filesS3Config.SSE = *mmconfig.FileSettings.AmazonS3SSE
|
||||
}
|
||||
if mmconfig.FileSettings.AmazonS3Trace != nil {
|
||||
filesS3Config.Trace = *mmconfig.FileSettings.AmazonS3Trace
|
||||
}
|
||||
|
||||
server, err := server.New(&config.Configuration{
|
||||
ServerRoot: *mmconfig.ServiceSettings.SiteURL + "/plugins/focalboard",
|
||||
Port: -1,
|
||||
DBType: *mmconfig.SqlSettings.DriverName,
|
||||
DBConfigString: *mmconfig.SqlSettings.DataSource,
|
||||
DBTablePrefix: "focalboard_",
|
||||
UseSSL: false,
|
||||
SecureCookie: true,
|
||||
WebPath: "./plugins/focalboard/pack",
|
||||
FilesDriver: *mmconfig.FileSettings.DriverName,
|
||||
FilesPath: *mmconfig.FileSettings.Directory,
|
||||
FilesS3Config: filesS3Config,
|
||||
Telemetry: true,
|
||||
WebhookUpdate: []string{},
|
||||
SessionExpireTime: 2592000,
|
||||
SessionRefreshTime: 18000,
|
||||
LocalOnly: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: "",
|
||||
AuthMode: "mattermost",
|
||||
}, "")
|
||||
if err != nil {
|
||||
fmt.Println("ERROR INITIALIZING THE SERVER", err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.wsHub = &WSHub{API: p.API}
|
||||
server.SetWSHub(p.wsHub)
|
||||
p.server = server
|
||||
return server.Start()
|
||||
}
|
||||
|
||||
func (p *Plugin) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) {
|
||||
if ev.Id == "websocket_event" {
|
||||
p.wsHub.handleWSMessage(ev.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) OnDeactivate() error {
|
||||
return p.server.Shutdown()
|
||||
}
|
||||
|
||||
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
|
||||
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
router := p.server.GetRootRouter()
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
28
mattermost-plugin/server/plugin_test.go
Normal file
28
mattermost-plugin/server/plugin_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
plugin := Plugin{}
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
plugin.ServeHTTP(nil, w, r)
|
||||
|
||||
result := w.Result()
|
||||
assert.NotNil(result)
|
||||
defer result.Body.Close()
|
||||
bodyBytes, err := ioutil.ReadAll(result.Body)
|
||||
assert.Nil(err)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
assert.Equal("Hello, world!", bodyString)
|
||||
}
|
669
mattermost-plugin/webapp/.eslintrc.json
Normal file
669
mattermost-plugin/webapp/.eslintrc.json
Normal file
@ -0,0 +1,669 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"impliedStrict": true,
|
||||
"modules": true,
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"import"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jquery": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"jest": true,
|
||||
"describe": true,
|
||||
"it": true,
|
||||
"expect": true,
|
||||
"before": true,
|
||||
"after": true,
|
||||
"beforeEach": true
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "webpack"
|
||||
},
|
||||
"rules": {
|
||||
"array-bracket-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"array-callback-return": 2,
|
||||
"arrow-body-style": 0,
|
||||
"arrow-parens": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"arrow-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [
|
||||
2,
|
||||
"1tbs",
|
||||
{
|
||||
"allowSingleLine": false
|
||||
}
|
||||
],
|
||||
"capitalized-comments": 0,
|
||||
"class-methods-use-this": 0,
|
||||
"comma-dangle": [
|
||||
2,
|
||||
"always-multiline"
|
||||
],
|
||||
"comma-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"comma-style": [
|
||||
2,
|
||||
"last"
|
||||
],
|
||||
"complexity": [
|
||||
0,
|
||||
10
|
||||
],
|
||||
"computed-property-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"consistent-return": 2,
|
||||
"consistent-this": [
|
||||
2,
|
||||
"self"
|
||||
],
|
||||
"constructor-super": 2,
|
||||
"curly": [
|
||||
2,
|
||||
"all"
|
||||
],
|
||||
"dot-location": [
|
||||
2,
|
||||
"object"
|
||||
],
|
||||
"dot-notation": 2,
|
||||
"eqeqeq": [
|
||||
2,
|
||||
"smart"
|
||||
],
|
||||
"func-call-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"func-name-matching": 0,
|
||||
"func-names": 2,
|
||||
"func-style": [
|
||||
2,
|
||||
"declaration",
|
||||
{
|
||||
"allowArrowFunctions": true
|
||||
}
|
||||
],
|
||||
"generator-star-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"global-require": 2,
|
||||
"guard-for-in": 2,
|
||||
"id-blacklist": 0,
|
||||
"import/no-unresolved": 2,
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
[
|
||||
"internal",
|
||||
"parent"
|
||||
],
|
||||
"sibling",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
2,
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 0
|
||||
}
|
||||
],
|
||||
"jsx-quotes": [
|
||||
2,
|
||||
"prefer-single"
|
||||
],
|
||||
"key-spacing": [
|
||||
2,
|
||||
{
|
||||
"beforeColon": false,
|
||||
"afterColon": true,
|
||||
"mode": "strict"
|
||||
}
|
||||
],
|
||||
"keyword-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": true,
|
||||
"after": true,
|
||||
"overrides": {}
|
||||
}
|
||||
],
|
||||
"line-comment-position": 0,
|
||||
"linebreak-style": 2,
|
||||
"lines-around-comment": [
|
||||
2,
|
||||
{
|
||||
"beforeBlockComment": true,
|
||||
"beforeLineComment": true,
|
||||
"allowBlockStart": true,
|
||||
"allowBlockEnd": true
|
||||
}
|
||||
],
|
||||
"max-lines": [
|
||||
1,
|
||||
{
|
||||
"max": 450,
|
||||
"skipBlankLines": true,
|
||||
"skipComments": false
|
||||
}
|
||||
],
|
||||
"max-nested-callbacks": [
|
||||
2,
|
||||
{
|
||||
"max": 2
|
||||
}
|
||||
],
|
||||
"max-statements-per-line": [
|
||||
2,
|
||||
{
|
||||
"max": 1
|
||||
}
|
||||
],
|
||||
"multiline-ternary": [
|
||||
1,
|
||||
"never"
|
||||
],
|
||||
"new-cap": 2,
|
||||
"new-parens": 2,
|
||||
"newline-before-return": 0,
|
||||
"newline-per-chained-call": 0,
|
||||
"no-alert": 2,
|
||||
"no-array-constructor": 2,
|
||||
"no-await-in-loop": 2,
|
||||
"no-caller": 2,
|
||||
"no-case-declarations": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-compare-neg-zero": 2,
|
||||
"no-cond-assign": [
|
||||
2,
|
||||
"except-parens"
|
||||
],
|
||||
"no-confusing-arrow": 2,
|
||||
"no-console": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-constant-condition": 2,
|
||||
"no-debugger": 2,
|
||||
"no-div-regex": 2,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-duplicate-imports": [
|
||||
2,
|
||||
{
|
||||
"includeExports": true
|
||||
}
|
||||
],
|
||||
"no-else-return": 2,
|
||||
"no-empty": 2,
|
||||
"no-empty-function": 2,
|
||||
"no-empty-pattern": 2,
|
||||
"no-eval": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-label": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-extra-semi": 2,
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-global-assign": 2,
|
||||
"no-implicit-coercion": 2,
|
||||
"no-implicit-globals": 0,
|
||||
"no-implied-eval": 2,
|
||||
"no-inner-declarations": 0,
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-magic-numbers": [
|
||||
0,
|
||||
{
|
||||
"ignore": [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"enforceConst": true,
|
||||
"detectObjects": true
|
||||
}
|
||||
],
|
||||
"no-mixed-operators": [
|
||||
2,
|
||||
{
|
||||
"allowSamePrecedence": false
|
||||
}
|
||||
],
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-assign": 2,
|
||||
"no-multi-spaces": [
|
||||
2,
|
||||
{
|
||||
"exceptions": {
|
||||
"Property": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"no-multi-str": 0,
|
||||
"no-multiple-empty-lines": [
|
||||
2,
|
||||
{
|
||||
"max": 1
|
||||
}
|
||||
],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-condition": 2,
|
||||
"no-nested-ternary": 2,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-object": 2,
|
||||
"no-new-symbol": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-param-reassign": 2,
|
||||
"no-process-env": 2,
|
||||
"no-process-exit": 2,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-return-assign": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"no-return-await": 2,
|
||||
"no-script-url": 2,
|
||||
"no-self-assign": [
|
||||
2,
|
||||
{
|
||||
"props": true
|
||||
}
|
||||
],
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": [
|
||||
2,
|
||||
{
|
||||
"hoist": "functions"
|
||||
}
|
||||
],
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-tabs": 0,
|
||||
"no-template-curly-in-string": 2,
|
||||
"no-ternary": 0,
|
||||
"no-this-before-super": 2,
|
||||
"no-throw-literal": 2,
|
||||
"no-trailing-spaces": [
|
||||
2,
|
||||
{
|
||||
"skipBlankLines": false
|
||||
}
|
||||
],
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 2,
|
||||
"no-underscore-dangle": 2,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unmodified-loop-condition": 2,
|
||||
"no-unneeded-ternary": [
|
||||
2,
|
||||
{
|
||||
"defaultAssignment": false
|
||||
}
|
||||
],
|
||||
"no-unreachable": 2,
|
||||
"no-unsafe-finally": 2,
|
||||
"no-unsafe-negation": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"classes": false,
|
||||
"functions": false,
|
||||
"variables": false
|
||||
}
|
||||
],
|
||||
"no-useless-computed-key": 2,
|
||||
"no-useless-concat": 2,
|
||||
"no-useless-constructor": 2,
|
||||
"no-useless-escape": 2,
|
||||
"no-useless-rename": 2,
|
||||
"no-useless-return": 2,
|
||||
"no-var": 0,
|
||||
"no-void": 2,
|
||||
"no-warning-comments": 1,
|
||||
"no-whitespace-before-property": 2,
|
||||
"no-with": 2,
|
||||
"object-curly-newline": 0,
|
||||
"object-curly-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"object-property-newline": [
|
||||
2,
|
||||
{
|
||||
"allowMultiplePropertiesPerLine": true
|
||||
}
|
||||
],
|
||||
"object-shorthand": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"one-var": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"one-var-declaration-per-line": 0,
|
||||
"operator-assignment": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"operator-linebreak": [
|
||||
2,
|
||||
"after"
|
||||
],
|
||||
"padded-blocks": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"prefer-arrow-callback": 2,
|
||||
"prefer-const": 2,
|
||||
"prefer-destructuring": 0,
|
||||
"prefer-numeric-literals": 2,
|
||||
"prefer-promise-reject-errors": 2,
|
||||
"prefer-rest-params": 2,
|
||||
"prefer-spread": 2,
|
||||
"prefer-template": 0,
|
||||
"quote-props": [
|
||||
2,
|
||||
"as-needed"
|
||||
],
|
||||
"quotes": [
|
||||
2,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"radix": 2,
|
||||
"react/display-name": [
|
||||
0,
|
||||
{
|
||||
"ignoreTranspilerName": false
|
||||
}
|
||||
],
|
||||
"react/forbid-component-props": 0,
|
||||
"react/forbid-elements": [
|
||||
2,
|
||||
{
|
||||
"forbid": [
|
||||
"embed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"react/jsx-closing-bracket-location": [
|
||||
2,
|
||||
{
|
||||
"location": "tag-aligned"
|
||||
}
|
||||
],
|
||||
"react/jsx-curly-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"react/jsx-equals-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"react/jsx-filename-extension": 2,
|
||||
"react/jsx-first-prop-new-line": [
|
||||
2,
|
||||
"multiline"
|
||||
],
|
||||
"react/jsx-handler-names": 0,
|
||||
"react/jsx-indent": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"react/jsx-indent-props": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"react/jsx-key": 2,
|
||||
"react/jsx-max-props-per-line": [
|
||||
2,
|
||||
{
|
||||
"maximum": 1
|
||||
}
|
||||
],
|
||||
"react/jsx-no-bind": 0,
|
||||
"react/jsx-no-comment-textnodes": 2,
|
||||
"react/jsx-no-duplicate-props": [
|
||||
2,
|
||||
{
|
||||
"ignoreCase": false
|
||||
}
|
||||
],
|
||||
"react/jsx-no-literals": 2,
|
||||
"react/jsx-no-target-blank": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-pascal-case": 2,
|
||||
"react/jsx-tag-spacing": [
|
||||
2,
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "never",
|
||||
"afterOpening": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/no-array-index-key": 1,
|
||||
"react/no-children-prop": 2,
|
||||
"react/no-danger": 0,
|
||||
"react/no-danger-with-children": 2,
|
||||
"react/no-deprecated": 1,
|
||||
"react/no-did-mount-set-state": 2,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-direct-mutation-state": 2,
|
||||
"react/no-find-dom-node": 1,
|
||||
"react/no-is-mounted": 2,
|
||||
"react/no-multi-comp": [
|
||||
2,
|
||||
{
|
||||
"ignoreStateless": true
|
||||
}
|
||||
],
|
||||
"react/no-render-return-value": 2,
|
||||
"react/no-set-state": 0,
|
||||
"react/no-string-refs": 0,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/no-unknown-property": 2,
|
||||
"react/no-unused-prop-types": [
|
||||
1,
|
||||
{
|
||||
"skipShapeProps": true
|
||||
}
|
||||
],
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/prefer-stateless-function": 2,
|
||||
"react/prop-types": [
|
||||
2,
|
||||
{
|
||||
"ignore": [
|
||||
"location",
|
||||
"history",
|
||||
"component"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/require-default-props": 0,
|
||||
"react/require-optimization": 1,
|
||||
"react/require-render-return": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/sort-comp": 0,
|
||||
"react/style-prop-object": 2,
|
||||
"require-yield": 2,
|
||||
"rest-spread-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"semi-spacing": [
|
||||
2,
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"sort-imports": 0,
|
||||
"sort-keys": 0,
|
||||
"space-before-blocks": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
2,
|
||||
{
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"space-in-parens": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [
|
||||
2,
|
||||
{
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}
|
||||
],
|
||||
"symbol-description": 2,
|
||||
"template-curly-spacing": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"valid-typeof": [
|
||||
2,
|
||||
{
|
||||
"requireStringLiterals": false
|
||||
}
|
||||
],
|
||||
"vars-on-top": 0,
|
||||
"wrap-iife": [
|
||||
2,
|
||||
"outside"
|
||||
],
|
||||
"wrap-regex": 2,
|
||||
"yoda": [
|
||||
2,
|
||||
"never",
|
||||
{
|
||||
"exceptRange": false,
|
||||
"onlyEquality": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.tsx", "**/*.ts"],
|
||||
"extends": "plugin:@typescript-eslint/recommended",
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-ignore": 0,
|
||||
"@typescript-eslint/ban-types": 1,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/prefer-interface": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/indent": [
|
||||
2,
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 0
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
2,
|
||||
{
|
||||
"classes": false,
|
||||
"functions": false,
|
||||
"variables": false
|
||||
}
|
||||
],
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".jsx", ".tsx"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
3
mattermost-plugin/webapp/.gitignore
vendored
Normal file
3
mattermost-plugin/webapp/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.eslintcache
|
||||
junit.xml
|
||||
node_modules
|
1
mattermost-plugin/webapp/.npmrc
Normal file
1
mattermost-plugin/webapp/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
save-exact=true
|
46
mattermost-plugin/webapp/babel.config.js
Normal file
46
mattermost-plugin/webapp/babel.config.js
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const config = {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
chrome: 66,
|
||||
firefox: 60,
|
||||
edge: 42,
|
||||
safari: 12,
|
||||
},
|
||||
modules: false,
|
||||
corejs: 3,
|
||||
debug: false,
|
||||
useBuiltIns: 'usage',
|
||||
shippedProposals: true,
|
||||
}],
|
||||
['@babel/preset-react', {
|
||||
useBuiltIns: true,
|
||||
}],
|
||||
['@babel/typescript', {
|
||||
allExtensions: true,
|
||||
isTSX: true,
|
||||
}],
|
||||
['@emotion/babel-preset-css-prop'],
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'babel-plugin-typescript-to-proptypes',
|
||||
],
|
||||
};
|
||||
|
||||
// Jest needs module transformation
|
||||
config.env = {
|
||||
test: {
|
||||
presets: config.presets,
|
||||
plugins: config.plugins,
|
||||
},
|
||||
};
|
||||
config.env.test.presets[0][1].modules = 'auto';
|
||||
|
||||
module.exports = config;
|
1
mattermost-plugin/webapp/i18n/en.json
Normal file
1
mattermost-plugin/webapp/i18n/en.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
41389
mattermost-plugin/webapp/package-lock.json
generated
Normal file
41389
mattermost-plugin/webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
115
mattermost-plugin/webapp/package.json
Normal file
115
mattermost-plugin/webapp/package.json
Normal file
@ -0,0 +1,115 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"build:watch": "webpack --mode=production --watch",
|
||||
"debug": "webpack --mode=none",
|
||||
"debug:watch": "webpack --mode=development --watch",
|
||||
"lint": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --cache",
|
||||
"fix": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --fix --cache",
|
||||
"test": "jest --forceExit --detectOpenHandles --verbose",
|
||||
"test:watch": "jest --watch",
|
||||
"test-ci": "jest --forceExit --detectOpenHandles --maxWorkers=2",
|
||||
"check-types": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.10.4",
|
||||
"@babel/core": "7.10.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/polyfill": "7.10.4",
|
||||
"@babel/preset-env": "7.10.4",
|
||||
"@babel/preset-react": "7.10.4",
|
||||
"@babel/preset-typescript": "7.10.4",
|
||||
"@babel/runtime": "7.10.4",
|
||||
"@emotion/babel-preset-css-prop": "10.0.27",
|
||||
"@emotion/core": "10.0.28",
|
||||
"@types/enzyme": "3.10.5",
|
||||
"@types/jest": "26.0.4",
|
||||
"@types/node": "14.0.20",
|
||||
"@types/react": "16.9.41",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/react-intl": "3.0.0",
|
||||
"@types/react-redux": "7.1.9",
|
||||
"@types/react-router-dom": "5.1.5",
|
||||
"@types/react-transition-group": "4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.6.0",
|
||||
"@typescript-eslint/parser": "3.6.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "26.1.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-typescript-to-proptypes": "1.3.2",
|
||||
"css-loader": "3.6.0",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.2",
|
||||
"enzyme-to-json": "3.5.0",
|
||||
"eslint": "7.4.0",
|
||||
"eslint-import-resolver-webpack": "0.12.2",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-react": "7.20.3",
|
||||
"eslint-plugin-react-hooks": "4.0.6",
|
||||
"file-loader": "6.0.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"jest": "26.1.0",
|
||||
"jest-canvas-mock": "2.2.0",
|
||||
"jest-junit": "11.0.1",
|
||||
"mattermost-webapp": "github:mattermost/mattermost-webapp#23f5f93d9f12a7e2b5623e5cee6814366abd9a0f",
|
||||
"node-sass": "4.14.1",
|
||||
"sass-loader": "9.0.2",
|
||||
"style-loader": "1.2.1",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-cli": "3.3.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.6.5",
|
||||
"mattermost-redux": "5.27.0",
|
||||
"react": "16.13.1",
|
||||
"react-redux": "7.2.0",
|
||||
"redux": "4.0.5",
|
||||
"typescript": "3.9.6"
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/node_modules/enzyme-to-json/serializer"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/non_npm_dependencies/"
|
||||
],
|
||||
"clearMocks": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"lcov",
|
||||
"text-summary"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "identity-obj-proxy",
|
||||
"^.+\\.(css|less|scss)$": "identity-obj-proxy",
|
||||
"^.*i18n.*\\.(json)$": "<rootDir>/tests/i18n_mock.json",
|
||||
"^bundle-loader\\?lazy\\!(.*)$": "$1"
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"",
|
||||
"node_modules",
|
||||
"non_npm_dependencies"
|
||||
],
|
||||
"reporters": [
|
||||
"default",
|
||||
"jest-junit"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!react-native|react-router|mattermost-webapp)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/setup.tsx"
|
||||
],
|
||||
"testURL": "http://localhost:8065"
|
||||
}
|
||||
}
|
56
mattermost-plugin/webapp/src/index.tsx
Normal file
56
mattermost-plugin/webapp/src/index.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import {Store, Action} from 'redux';
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
|
||||
import manifest from './manifest';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {PluginRegistry} from './types/mattermost-webapp';
|
||||
|
||||
const focalboardIcon = (
|
||||
<svg
|
||||
className='LogoWithNameIcon Icon'
|
||||
viewBox='0 0 64 64'
|
||||
width='24px'
|
||||
height='24px'
|
||||
>
|
||||
<g opacity='0.56'>
|
||||
<path
|
||||
fill='rgba(var(--center-channel-color-rgb), 1)'
|
||||
d='M33.071,12.289C20.408,8.822,6.018,15.578,1.395,29.232 c-4.655,13.75,2.719,28.67,16.469,33.325c13.75,4.655,28.67-2.719,33.326-16.469c3.804-11.235-0.462-22.701-8.976-29.249 l-0.46,4.871l-0.001,0c4.631,4.896,6.709,11.941,4.325,18.985c-3.362,9.931-14.447,15.151-24.76,11.66 C11.005,48.865,5.37,37.985,8.731,28.054c2.975-8.788,11.998-13.715,20.743-12.625v-0.001L33.071,12.289L33.071,12.289z M26.896,28.777c3.456-0.665,6.986,2.754,5.762,6.37c-0.854,2.522-3.67,3.85-6.291,2.962c-2.62-0.887-4.052-3.651-3.197-6.174 C23.743,30.238,25.204,29.083,26.896,28.777L26.896,28.777z M25.611,23.833c-1.786,0.323-3.45,1.104-4.812,2.258 c-1.299,1.101-2.319,2.545-2.898,4.258c-0.879,2.597-0.579,5.323,0.617,7.632c1.206,2.329,3.325,4.234,6.07,5.164 c2.744,0.929,5.584,0.701,7.959-0.417c2.352-1.107,4.246-3.091,5.125-5.688c0.555-1.639,0.633-3.254,0.344-4.761 c-0.21-1.093-0.615-2.134-1.174-3.091l1.019-5.107c0.189,0.187,0.374,0.378,0.552,0.574c1.75,1.919,3.008,4.283,3.508,6.877 c0.415,2.154,0.304,4.457-0.484,6.784c-1.239,3.661-3.898,6.453-7.193,8.005c-3.273,1.541-7.175,1.858-10.93,0.588 c-3.754-1.271-6.661-3.895-8.326-7.108c-1.674-3.233-2.09-7.065-0.851-10.728c0.819-2.419,2.26-4.46,4.097-6.016 c1.88-1.593,4.181-2.673,6.656-3.125l-0.001-0.004c1.759-0.339,3.522-0.313,5.213,0.016l-3.583,3.761 c-0.294,0.028-0.588,0.071-0.883,0.127H25.611z'
|
||||
/>
|
||||
<polygon
|
||||
fill='rgba(var(--center-channel-color-rgb), 1)'
|
||||
points='37.495,11.658 36.79,8.44 41.066,0.207 43.683,4.611 48.803,4.434 44.185,12.48 40.902,13.697 29.542,34.491 26.057,32.594'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default class Plugin {
|
||||
channelHeaderButtonId: string;
|
||||
registry: PluginRegistry
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
public async initialize(registry: PluginRegistry, store: Store<GlobalState, Action<Record<string, unknown>>>) {
|
||||
this.registry = registry;
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(focalboardIcon, () => {
|
||||
const currentChannel = store.getState().entities.channels.currentChannelId;
|
||||
window.open(`${window.location.origin}/plugins/focalboard/workspace/${currentChannel}`);
|
||||
}, '', 'Focalboard Workspace');
|
||||
}
|
||||
|
||||
public uninitialize() {
|
||||
if (this.channelHeaderButtonId) {
|
||||
this.registry.unregisterComponent(this.channelHeaderButtonId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
registerPlugin(id: string, plugin: Plugin): void
|
||||
}
|
||||
}
|
||||
|
||||
window.registerPlugin(manifest.id, new Plugin());
|
13
mattermost-plugin/webapp/src/manifest.test.tsx
Normal file
13
mattermost-plugin/webapp/src/manifest.test.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import manifest, {id, version} from './manifest';
|
||||
|
||||
test('Plugin manifest, id and version are defined', () => {
|
||||
expect(manifest).toBeDefined();
|
||||
expect(manifest.id).toBeDefined();
|
||||
expect(manifest.version).toBeDefined();
|
||||
});
|
||||
|
||||
// To ease migration, verify separate export of id and version.
|
||||
test('Plugin id and version are defined', () => {
|
||||
expect(id).toBeDefined();
|
||||
expect(version).toBeDefined();
|
||||
});
|
5
mattermost-plugin/webapp/src/manifest.ts
Normal file
5
mattermost-plugin/webapp/src/manifest.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import manifest from '../../plugin.json';
|
||||
|
||||
export default manifest;
|
||||
export const id = manifest.id;
|
||||
export const version = manifest.version;
|
5
mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts
vendored
Normal file
5
mattermost-plugin/webapp/src/types/mattermost-webapp/index.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export interface PluginRegistry {
|
||||
registerPostTypeComponent(typeName: string, component: React.ElementType)
|
||||
|
||||
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
||||
}
|
1
mattermost-plugin/webapp/tests/i18n_mock.json
Normal file
1
mattermost-plugin/webapp/tests/i18n_mock.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
4
mattermost-plugin/webapp/tests/setup.tsx
Normal file
4
mattermost-plugin/webapp/tests/setup.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import 'mattermost-webapp/tests/setup';
|
34
mattermost-plugin/webapp/tsconfig.json
Normal file
34
mattermost-plugin/webapp/tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react",
|
||||
"baseUrl": "./src",
|
||||
"typeRoots": [ "./src/types", "./node_modules/@types"],
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"!node_modules/@types"
|
||||
]
|
||||
}
|
99
mattermost-plugin/webapp/webpack.config.js
Normal file
99
mattermost-plugin/webapp/webpack.config.js
Normal file
@ -0,0 +1,99 @@
|
||||
const exec = require('child_process').exec;
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const PLUGIN_ID = require('../plugin.json').id;
|
||||
|
||||
const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
|
||||
let mode = 'production';
|
||||
let devtool = '';
|
||||
if (NPM_TARGET === 'debug' || NPM_TARGET === 'debug:watch') {
|
||||
mode = 'development';
|
||||
devtool = 'source-map';
|
||||
}
|
||||
|
||||
const plugins = [];
|
||||
if (NPM_TARGET === 'build:watch' || NPM_TARGET === 'debug:watch') {
|
||||
plugins.push({
|
||||
apply: (compiler) => {
|
||||
compiler.hooks.watchRun.tap('WatchStartPlugin', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Change detected. Rebuilding webapp.');
|
||||
});
|
||||
compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
|
||||
exec('cd .. && make deploy-from-watch', (err, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
'./src/index.tsx',
|
||||
],
|
||||
resolve: {
|
||||
modules: [
|
||||
'src',
|
||||
'node_modules',
|
||||
path.resolve(__dirname),
|
||||
],
|
||||
extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx|ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
|
||||
// Babel configuration is in babel.config.js because jest requires it to be there.
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(scss|css)$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sassOptions: {
|
||||
includePaths: ['node_modules/compass-mixins/lib', 'sass'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
react: 'React',
|
||||
redux: 'Redux',
|
||||
'react-redux': 'ReactRedux',
|
||||
'prop-types': 'PropTypes',
|
||||
'react-bootstrap': 'ReactBootstrap',
|
||||
'react-router-dom': 'ReactRouterDom',
|
||||
},
|
||||
output: {
|
||||
devtoolNamespace: PLUGIN_ID,
|
||||
path: path.join(__dirname, '/dist'),
|
||||
publicPath: '/',
|
||||
filename: 'main.js',
|
||||
},
|
||||
devtool,
|
||||
mode,
|
||||
plugins,
|
||||
};
|
@ -33,16 +33,11 @@ const (
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
// REST APIs
|
||||
|
||||
type WorkspaceAuthenticator interface {
|
||||
DoesUserHaveWorkspaceAccess(session *model.Session, workspaceID string) bool
|
||||
GetWorkspace(session *model.Session, workspaceID string) *model.Workspace
|
||||
}
|
||||
|
||||
type API struct {
|
||||
appBuilder func() *app.App
|
||||
authService string
|
||||
singleUserToken string
|
||||
WorkspaceAuthenticator WorkspaceAuthenticator
|
||||
appBuilder func() *app.App
|
||||
authService string
|
||||
singleUserToken string
|
||||
MattermostAuth bool
|
||||
}
|
||||
|
||||
func NewAPI(appBuilder func() *app.App, singleUserToken string, authService string) *API {
|
||||
@ -138,14 +133,21 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
|
||||
ctx := r.Context()
|
||||
session, _ := ctx.Value("session").(*model.Session)
|
||||
|
||||
if a.WorkspaceAuthenticator == nil {
|
||||
// Native auth: always use root workspace
|
||||
if a.MattermostAuth {
|
||||
// Workspace auth
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspaceID"]
|
||||
|
||||
container := store.Container{
|
||||
WorkspaceID: "0",
|
||||
WorkspaceID: workspaceID,
|
||||
}
|
||||
|
||||
// Has session
|
||||
if session != nil {
|
||||
if workspaceID == "0" {
|
||||
return &container, nil
|
||||
}
|
||||
|
||||
// Has session and access to workspace
|
||||
if session != nil && a.app().DoesUserHaveWorkspaceAccess(session.UserID, container.WorkspaceID) {
|
||||
return &container, nil
|
||||
}
|
||||
|
||||
@ -157,16 +159,13 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
|
||||
return nil, errors.New("Access denied to workspace")
|
||||
}
|
||||
|
||||
// Workspace auth
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspaceID"]
|
||||
|
||||
// Native auth: always use root workspace
|
||||
container := store.Container{
|
||||
WorkspaceID: workspaceID,
|
||||
WorkspaceID: "0",
|
||||
}
|
||||
|
||||
// Has session and access to workspace
|
||||
if session != nil && a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, container.WorkspaceID) {
|
||||
// Has session
|
||||
if session != nil {
|
||||
return &container, nil
|
||||
}
|
||||
|
||||
@ -896,18 +895,21 @@ func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
var workspace *model.Workspace
|
||||
var err error
|
||||
|
||||
if a.WorkspaceAuthenticator != nil {
|
||||
if a.MattermostAuth {
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspaceID"]
|
||||
|
||||
ctx := r.Context()
|
||||
session := ctx.Value("session").(*model.Session)
|
||||
if !a.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, workspaceID) {
|
||||
if !a.app().DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) {
|
||||
errorResponse(w, http.StatusUnauthorized, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
workspace = a.WorkspaceAuthenticator.GetWorkspace(session, workspaceID)
|
||||
workspace, err = a.app().GetWorkspace(workspaceID)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, "", err)
|
||||
}
|
||||
if workspace == nil {
|
||||
errorResponse(w, http.StatusUnauthorized, "", nil)
|
||||
return
|
||||
@ -1029,8 +1031,13 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
filePath := a.app().GetFilePath(workspaceID, rootID, filename)
|
||||
http.ServeFile(w, r, filePath)
|
||||
fileReader, err := a.app().GetFileReader(workspaceID, rootID, filename)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
defer fileReader.Close()
|
||||
http.ServeContent(w, r, filename, time.Now(), fileReader)
|
||||
}
|
||||
|
||||
// FileUploadResponse is the response to a file upload
|
||||
|
@ -371,6 +371,23 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if a.MattermostAuth && r.Header.Get("Mattermost-User-Id") != "" {
|
||||
userID := r.Header.Get("Mattermost-User-Id")
|
||||
now := time.Now().Unix()
|
||||
session := &model.Session{
|
||||
ID: userID,
|
||||
Token: userID,
|
||||
UserID: userID,
|
||||
AuthService: a.authService,
|
||||
Props: map[string]interface{}{},
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "session", session)
|
||||
handler(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
session, err := a.app().GetSession(token)
|
||||
if err != nil {
|
||||
if required {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@ -14,7 +14,7 @@ type App struct {
|
||||
store store.Store
|
||||
auth *auth.Auth
|
||||
wsServer *ws.Server
|
||||
filesBackend filesstore.FileBackend
|
||||
filesBackend filestore.FileBackend
|
||||
webhook *webhook.Client
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ func New(
|
||||
store store.Store,
|
||||
auth *auth.Auth,
|
||||
wsServer *ws.Server,
|
||||
filesBackend filesstore.FileBackend,
|
||||
filesBackend filestore.FileBackend,
|
||||
webhook *webhook.Client,
|
||||
) *App {
|
||||
return &App{
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/store/mockstore"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore/mocks"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -22,7 +22,7 @@ func TestGetParentID(t *testing.T) {
|
||||
store := mockstore.NewMockStore(ctrl)
|
||||
auth := auth.New(&cfg, store)
|
||||
sessionToken := "TESTTOKEN"
|
||||
wsserver := ws.NewServer(auth, sessionToken)
|
||||
wsserver := ws.NewServer(auth, sessionToken, false)
|
||||
webhook := webhook.NewClient(&cfg)
|
||||
app := New(&cfg, store, auth, wsserver, &mocks.FileBackend{}, webhook)
|
||||
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
)
|
||||
|
||||
func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) {
|
||||
@ -30,26 +31,34 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
|
||||
return createdFilename, nil
|
||||
}
|
||||
|
||||
func (a *App) GetFilePath(workspaceID, rootID, filename string) string {
|
||||
folderPath := a.config.FilesPath
|
||||
rootPath := filepath.Join(folderPath, workspaceID, rootID)
|
||||
|
||||
filePath := filepath.Join(rootPath, filename)
|
||||
|
||||
func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
|
||||
filePath := filepath.Join(workspaceID, rootID, filename)
|
||||
exists, err := a.filesBackend.FileExists(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// FIXUP: Check the deprecated old location
|
||||
if workspaceID == "0" && !fileExists(filePath) {
|
||||
oldFilePath := filepath.Join(folderPath, filename)
|
||||
if fileExists(oldFilePath) {
|
||||
err := os.Rename(oldFilePath, filePath)
|
||||
if workspaceID == "0" && !exists {
|
||||
oldExists, err := a.filesBackend.FileExists(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oldExists {
|
||||
err := a.filesBackend.MoveFile(filename, filePath)
|
||||
if err != nil {
|
||||
log.Printf("ERROR moving old file from '%s' to '%s'", oldFilePath, filePath)
|
||||
log.Printf("ERROR moving old file from '%s' to '%s'", filename, filePath)
|
||||
} else {
|
||||
log.Printf("Moved old file from '%s' to '%s'", oldFilePath, filePath)
|
||||
log.Printf("Moved old file from '%s' to '%s'", filename, filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filePath
|
||||
reader, err := a.filesBackend.Reader(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
|
@ -33,7 +33,7 @@ func (a *App) GetRootWorkspace() (*model.Workspace, error) {
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (a *App) getWorkspace(ID string) (*model.Workspace, error) {
|
||||
func (a *App) GetWorkspace(ID string) (*model.Workspace, error) {
|
||||
workspace, err := a.store.GetWorkspace(ID)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@ -44,6 +44,10 @@ func (a *App) getWorkspace(ID string) (*model.Workspace, error) {
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
|
||||
return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID)
|
||||
}
|
||||
|
||||
func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error {
|
||||
return a.store.UpsertWorkspaceSettings(workspace)
|
||||
}
|
||||
|
@ -58,3 +58,11 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
|
||||
hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasAccess
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ go 1.15
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/google/uuid v1.2.0
|
||||
@ -12,7 +13,7 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattermost/mattermost-server/v5 v5.33.2
|
||||
github.com/mattermost/mattermost-server/v5 v5.34.2
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/oklog/run v1.1.0
|
||||
|
105
server/go.sum
105
server/go.sum
@ -101,7 +101,7 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
|
||||
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
|
||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZFIkk4B1FouGxqlogyMyU6BwY/UiVmmbbzz9Up8=
|
||||
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@ -113,7 +113,7 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ
|
||||
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.36.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.37.18/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
@ -152,7 +152,6 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -246,7 +245,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
@ -264,14 +262,14 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.9.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20210130063903-47dfef350d96/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag=
|
||||
@ -289,10 +287,10 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo=
|
||||
github.com/go-redis/redis/v8 v8.4.9/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo=
|
||||
github.com/go-redis/redis/v8 v8.6.0/go.mod h1:DQ9q4Rk2HtwkrwVrdgmphoOQDMfpvcd/nHEwRsicg8s=
|
||||
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
|
||||
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
@ -378,7 +376,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
@ -387,8 +384,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
@ -508,6 +505,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
@ -546,6 +544,9 @@ github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
||||
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||
@ -585,6 +586,9 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||
@ -601,8 +605,8 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
|
||||
github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P8=
|
||||
github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs=
|
||||
github.com/mattermost/mattermost-server/v5 v5.33.2 h1:67pG6GAQ+jGU2HZkMu5uexLjeExt6rqIhovv6ksKgpk=
|
||||
github.com/mattermost/mattermost-server/v5 v5.33.2/go.mod h1:VjeLX3HlgzR1FKYR6Ju0tR+9dAhhVMlM/s8a7rfbIMI=
|
||||
github.com/mattermost/mattermost-server/v5 v5.34.2 h1:5EjIo/UjwksCxbL6nDS7XNRghmqjdf2vH5wUFgg7EAo=
|
||||
github.com/mattermost/mattermost-server/v5 v5.34.2/go.mod h1:UGbMK9bMzmqGyQQ3N/3I6FDpTwJsL/wMYaO+4zctzBA=
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
@ -617,17 +621,16 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
@ -639,15 +642,15 @@ github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00v
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.39/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
|
||||
github.com/minio/md5-simd v1.1.1 h1:9ojcLbuZ4gXbB2sX53MKn8JUZ0sB/2wfwsEcRw+I08U=
|
||||
github.com/minio/md5-simd v1.1.1/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
|
||||
github.com/minio/minio-go/v7 v7.0.7 h1:Qld/xb8C1Pwbu0jU46xAceyn9xXKCMW+3XfNbpmTB70=
|
||||
github.com/minio/minio-go/v7 v7.0.7/go.mod h1:pEZBUa+L2m9oECoIA6IcSK8bv/qggtQVLovjeKK5jYc=
|
||||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.10 h1:1oUKe4EOPUEhw2qnPQaPsJ0lmVTYLFu03SiItauXs94=
|
||||
github.com/minio/minio-go/v7 v7.0.10/go.mod h1:td4gW1ldOsj1PbSNS+WYK43j+P1XVhX/8W8awaYlBFo=
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sio v0.2.1/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@ -703,6 +706,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/olivere/elastic v6.2.35+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -711,13 +715,13 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/oov/psd v0.0.0-20201203182240-dad9002861d9/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@ -796,8 +800,9 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
|
||||
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/common v0.17.0 h1:kDIZLI74SS+3tedSvEkykgBkD7txMxaJAPj8DtJUKYA=
|
||||
github.com/prometheus/common v0.17.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@ -806,8 +811,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.3.0 h1:Uehi/mxLK0eiUc0H0++5tpMGTexB8wZ598MIgU8VpDM=
|
||||
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
@ -871,12 +876,12 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
|
||||
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
|
||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0=
|
||||
@ -894,7 +899,7 @@ github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@ -906,9 +911,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/splitio/go-client/v6 v6.0.1/go.mod h1:WHjkBtEEJFHOpgqnHSb/X+e4CgzTi1gPNkg+c7w1Izg=
|
||||
github.com/splitio/go-split-commons/v2 v2.0.1/go.mod h1:YsmZa0AE9DCTosPXekKBxYHzNXJHpZwID9FSfo5Tnis=
|
||||
github.com/splitio/go-toolkit/v3 v3.0.1/go.mod h1:HGgawLnM2RlM84zVRbATpPMjF7H6u9CUYG6RlpwOlOk=
|
||||
github.com/splitio/go-client/v6 v6.0.2/go.mod h1:JbxrPJiIJHdPi5alQx5bszNzNrdJR+er1ktd9RzGtgk=
|
||||
github.com/splitio/go-split-commons/v3 v3.0.0/go.mod h1:6YEmYZnfrqV1Fp0vo4JL26zhzsDPD7p/OwFWy1LW2n4=
|
||||
github.com/splitio/go-toolkit/v4 v4.0.0/go.mod h1:EdIHN0yzB1GTXDYQc0KdKvnjkO/jfUM2YqHVYfhD3Wo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
@ -957,7 +962,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
@ -967,8 +972,8 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
|
||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/vmihailenco/msgpack/v5 v5.2.0/go.mod h1:fEM7KuHcnm0GvDCztRpw9hV0PuoO2ciTismP6vjggcM=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wiggin77/cfg v1.0.2 h1:NBUX+iJRr+RTncTqTNvajHwzduqbhCQjEqxLHr6Fk7A=
|
||||
github.com/wiggin77/cfg v1.0.2/go.mod h1:b3gotba2e5bXTqTW48DwIFoLc+4lWKP7WPi/CdvZ4aE=
|
||||
github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg=
|
||||
@ -1016,7 +1021,10 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0=
|
||||
go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
|
||||
go.opentelemetry.io/otel v0.17.0/go.mod h1:Oqtdxmf7UtEvL037ohlgnaYa1h7GtMh0NcSd9eqkC9s=
|
||||
go.opentelemetry.io/otel/metric v0.17.0/go.mod h1:hUz9lH1rNXyEwWAhIWCMFWKhYtpASgSnObJFnU26dJ0=
|
||||
go.opentelemetry.io/otel/oteltest v0.17.0/go.mod h1:JT/LGFxPwpN+nlsTiinSYjdIx3hZIGqHCpChcIZmdoE=
|
||||
go.opentelemetry.io/otel/trace v0.17.0/go.mod h1:bIujpqg6ZL6xUTubIUgziI1jSaUPthmabA/ygf/6Cfg=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
@ -1046,7 +1054,6 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -1056,7 +1063,7 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1075,8 +1082,8 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -1154,7 +1161,7 @@ golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@ -1177,6 +1184,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1240,7 +1249,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225014209-683adc9d29d7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
@ -1320,6 +1332,7 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1400,8 +1413,8 @@ google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc h1:tsmkSntchraIEmbGgYaZGZ8LVIidKCWEmPwEnSQSalA=
|
||||
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 h1:bXUwz2WkXXrXgiLxww3vWmoSHLOGv4ipdPdTvKymcKw=
|
||||
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
@ -1425,8 +1438,8 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -34,6 +34,7 @@ func getTestConfig() *config.Configuration {
|
||||
DBConfigString: connectionString,
|
||||
DBTablePrefix: "test_",
|
||||
WebPath: "./pack",
|
||||
FilesDriver: "local",
|
||||
FilesPath: "./files",
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,6 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mattermost/focalboard/server/einterfaces"
|
||||
)
|
||||
|
||||
func (s *Server) initHandlers() {
|
||||
cfg := s.config
|
||||
if cfg.AuthMode == "mattermost" && mattermostAuth != nil {
|
||||
log.Println("Using Mattermost Auth")
|
||||
params := einterfaces.MattermostAuthParameters{
|
||||
ServerRoot: cfg.ServerRoot,
|
||||
MattermostURL: cfg.MattermostURL,
|
||||
ClientID: cfg.MattermostClientID,
|
||||
ClientSecret: cfg.MattermostClientSecret,
|
||||
UseSecureCookie: cfg.SecureCookie,
|
||||
}
|
||||
mmauthHandler := mattermostAuth(params, s.store)
|
||||
log.Println("CREATING AUTH")
|
||||
s.webServer.AddRoutes(mmauthHandler)
|
||||
log.Println("ADDING ROUTES")
|
||||
s.api.WorkspaceAuthenticator = mmauthHandler
|
||||
log.Println("SETTING THE AUTHENTICATOR")
|
||||
}
|
||||
s.api.MattermostAuth = cfg.AuthMode == "mattermost"
|
||||
}
|
||||
|
@ -24,12 +24,13 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/prometheus"
|
||||
"github.com/mattermost/focalboard/server/services/scheduler"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/services/telemetry"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/web"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/oklog/run"
|
||||
)
|
||||
@ -46,7 +47,7 @@ type Server struct {
|
||||
wsServer *ws.Server
|
||||
webServer *web.Server
|
||||
store store.Store
|
||||
filesBackend filesstore.FileBackend
|
||||
filesBackend filestore.FileBackend
|
||||
telemetry *telemetry.Service
|
||||
logger *zap.Logger
|
||||
cleanUpSessionsTask *scheduler.ScheduledTask
|
||||
@ -65,21 +66,40 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix)
|
||||
var db store.Store
|
||||
db, err = sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix)
|
||||
if err != nil {
|
||||
log.Print("Unable to start the database", err)
|
||||
return nil, err
|
||||
}
|
||||
if cfg.AuthMode == "mattermost" {
|
||||
layeredStore, err := mattermostauthlayer.New(cfg.DBType, cfg.DBConfigString, db)
|
||||
if err != nil {
|
||||
log.Print("Unable to start the database", err)
|
||||
return nil, err
|
||||
}
|
||||
db = layeredStore
|
||||
}
|
||||
|
||||
authenticator := auth.New(cfg, db)
|
||||
|
||||
wsServer := ws.NewServer(authenticator, singleUserToken)
|
||||
wsServer := ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == "mattermost")
|
||||
|
||||
filesBackendSettings := filesstore.FileBackendSettings{}
|
||||
filesBackendSettings.DriverName = "local"
|
||||
filesBackendSettings := filestore.FileBackendSettings{}
|
||||
filesBackendSettings.DriverName = cfg.FilesDriver
|
||||
filesBackendSettings.Directory = cfg.FilesPath
|
||||
filesBackend, appErr := filesstore.NewFileBackend(filesBackendSettings)
|
||||
filesBackendSettings.AmazonS3AccessKeyId = cfg.FilesS3Config.AccessKeyId
|
||||
filesBackendSettings.AmazonS3SecretAccessKey = cfg.FilesS3Config.SecretAccessKey
|
||||
filesBackendSettings.AmazonS3Bucket = cfg.FilesS3Config.Bucket
|
||||
filesBackendSettings.AmazonS3PathPrefix = cfg.FilesS3Config.PathPrefix
|
||||
filesBackendSettings.AmazonS3Region = cfg.FilesS3Config.Region
|
||||
filesBackendSettings.AmazonS3Endpoint = cfg.FilesS3Config.Endpoint
|
||||
filesBackendSettings.AmazonS3SSL = cfg.FilesS3Config.SSL
|
||||
filesBackendSettings.AmazonS3SignV2 = cfg.FilesS3Config.SignV2
|
||||
filesBackendSettings.AmazonS3SSE = cfg.FilesS3Config.SSE
|
||||
filesBackendSettings.AmazonS3Trace = cfg.FilesS3Config.Trace
|
||||
|
||||
filesBackend, appErr := filestore.NewFileBackend(filesBackendSettings)
|
||||
if appErr != nil {
|
||||
log.Print("Unable to initialize the files storage")
|
||||
|
||||
@ -302,3 +322,7 @@ func (s *Server) stopLocalModeServer() {
|
||||
func (s *Server) GetRootRouter() *mux.Router {
|
||||
return s.webServer.Router()
|
||||
}
|
||||
|
||||
func (s *Server) SetWSHub(hub ws.Hub) {
|
||||
s.wsServer.SetHub(hub)
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ func TestParseAuthTokenFromRequest(t *testing.T) {
|
||||
}
|
||||
if tc.cookie != "" {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: SESSION_COOKIE_TOKEN,
|
||||
Name: "FOCALBOARDAUTHTOKEN",
|
||||
Value: tc.cookie,
|
||||
})
|
||||
}
|
||||
|
@ -11,31 +11,43 @@ const (
|
||||
DefaultPort = 8000
|
||||
)
|
||||
|
||||
type AmazonS3Config struct {
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
Bucket string
|
||||
PathPrefix string
|
||||
Region string
|
||||
Endpoint string
|
||||
SSL bool
|
||||
SignV2 bool
|
||||
SSE bool
|
||||
Trace bool
|
||||
}
|
||||
|
||||
// Configuration is the app configuration stored in a json file.
|
||||
type Configuration struct {
|
||||
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
DBType string `json:"dbtype" mapstructure:"dbtype"`
|
||||
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
|
||||
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
|
||||
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
|
||||
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
|
||||
WebPath string `json:"webpath" mapstructure:"webpath"`
|
||||
FilesPath string `json:"filespath" mapstructure:"filespath"`
|
||||
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
|
||||
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
|
||||
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
|
||||
Secret string `json:"secret" mapstructure:"secret"`
|
||||
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
|
||||
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
|
||||
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
|
||||
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
|
||||
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
|
||||
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
DBType string `json:"dbtype" mapstructure:"dbtype"`
|
||||
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
|
||||
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
|
||||
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
|
||||
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
|
||||
WebPath string `json:"webpath" mapstructure:"webpath"`
|
||||
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
|
||||
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
|
||||
FilesPath string `json:"filespath" mapstructure:"filespath"`
|
||||
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
|
||||
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
|
||||
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
|
||||
Secret string `json:"secret" mapstructure:"secret"`
|
||||
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
|
||||
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
|
||||
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
|
||||
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
|
||||
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
|
||||
|
||||
AuthMode string `json:"authMode" mapstructure:"authMode"`
|
||||
MattermostURL string `json:"mattermostURL" mapstructure:"mattermostURL"`
|
||||
MattermostClientID string `json:"mattermostClientID" mapstructure:"mattermostClientID"`
|
||||
MattermostClientSecret string `json:"mattermostClientSecret" mapstructure:"mattermostClientSecret"`
|
||||
AuthMode string `json:"authMode" mapstructure:"authMode"`
|
||||
}
|
||||
|
||||
// ReadConfigFile read the configuration from the filesystem.
|
||||
@ -53,6 +65,7 @@ func ReadConfigFile() (*Configuration, error) {
|
||||
viper.SetDefault("SecureCookie", false)
|
||||
viper.SetDefault("WebPath", "./pack")
|
||||
viper.SetDefault("FilesPath", "./files")
|
||||
viper.SetDefault("FilesDriver", "local")
|
||||
viper.SetDefault("Telemetry", true)
|
||||
viper.SetDefault("WebhookUpdate", nil)
|
||||
viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime
|
||||
@ -83,9 +96,5 @@ func ReadConfigFile() (*Configuration, error) {
|
||||
|
||||
func removeSecurityData(config Configuration) Configuration {
|
||||
clean := config
|
||||
clean.Secret = "hidden"
|
||||
clean.MattermostClientID = "hidden"
|
||||
clean.MattermostClientSecret = "hidden"
|
||||
|
||||
return clean
|
||||
}
|
||||
|
254
server/services/store/mattermostauthlayer/mattermostauthlayer.go
Normal file
254
server/services/store/mattermostauthlayer/mattermostauthlayer.go
Normal file
@ -0,0 +1,254 @@
|
||||
package mattermostauthlayer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
const (
|
||||
mysqlDBType = "mysql"
|
||||
sqliteDBType = "sqlite3"
|
||||
postgresDBType = "postgres"
|
||||
)
|
||||
|
||||
// Store represents the abstraction of the data storage.
|
||||
type MattermostAuthLayer struct {
|
||||
store.Store
|
||||
dbType string
|
||||
mmDB *sql.DB
|
||||
}
|
||||
|
||||
// New creates a new SQL implementation of the store.
|
||||
func New(dbType, connectionString string, store store.Store) (*MattermostAuthLayer, error) {
|
||||
log.Println("connectDatabase", dbType, connectionString)
|
||||
var err error
|
||||
|
||||
db, err := sql.Open(dbType, connectionString)
|
||||
if err != nil {
|
||||
log.Print("connectDatabase: ", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
log.Printf(`Database Ping failed: %v`, err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layer := &MattermostAuthLayer{
|
||||
Store: store,
|
||||
dbType: dbType,
|
||||
mmDB: db,
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// Shutdown close the connection with the store.
|
||||
func (l *MattermostAuthLayer) Shutdown() error {
|
||||
err := l.Store.Shutdown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.mmDB.Close()
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("count(*)").
|
||||
From("Users").
|
||||
Where(sq.Eq{"deleteAt": 0})
|
||||
row := query.QueryRow()
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) getUserByCondition(condition sq.Eq) (*model.User, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("id", "username", "email", "password", "MFASecret as mfa_secret", "AuthService as auth_service", "COALESCE(AuthData, '') as auth_data", "props", "CreateAt as create_at", "UpdateAt as update_at", "DeleteAt as delete_at").
|
||||
From("Users").
|
||||
Where(sq.Eq{"deleteAt": 0}).
|
||||
Where(condition)
|
||||
row := query.QueryRow()
|
||||
user := model.User{}
|
||||
|
||||
var propsBytes []byte
|
||||
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(propsBytes, &user.Props)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetUserById(userID string) (*model.User, error) {
|
||||
return s.getUserByCondition(sq.Eq{"id": userID})
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetUserByEmail(email string) (*model.User, error) {
|
||||
return s.getUserByCondition(sq.Eq{"email": email})
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetUserByUsername(username string) (*model.User, error) {
|
||||
return s.getUserByCondition(sq.Eq{"username": username})
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) CreateUser(user *model.User) error {
|
||||
return errors.New("no user creation allowed from focalboard, create it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) UpdateUser(user *model.User) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) UpdateUserPassword(username, password string) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
// GetActiveUserCount returns the number of users with active sessions within N seconds ago
|
||||
func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("count(distinct userId)").
|
||||
From("Sessions").
|
||||
Where(sq.Gt{"LastActivityAt": time.Now().Unix() - updatedSecondsAgo})
|
||||
|
||||
row := query.QueryRow()
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetSession(token string, expireTime int64) (*model.Session, error) {
|
||||
return nil, errors.New("sessions not used when using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) CreateSession(session *model.Session) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) RefreshSession(session *model.Session) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) UpdateSession(session *model.Session) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) DeleteSession(sessionId string) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) CleanUpSessions(expireTime int64) error {
|
||||
return errors.New("no update allowed from focalboard, update it using mattermost")
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetWorkspace(ID string) (*model.Workspace, error) {
|
||||
if ID == "0" {
|
||||
workspace := model.Workspace{
|
||||
ID: ID,
|
||||
Title: "",
|
||||
}
|
||||
|
||||
return &workspace, nil
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("DisplayName, Type").
|
||||
From("Channels").
|
||||
Where(sq.Eq{"ID": ID})
|
||||
|
||||
row := query.QueryRow()
|
||||
var displayName string
|
||||
var channelType string
|
||||
err := row.Scan(&displayName, &channelType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if channelType != "D" && channelType != "G" {
|
||||
return &model.Workspace{ID: ID, Title: displayName}, nil
|
||||
}
|
||||
|
||||
query = s.getQueryBuilder().
|
||||
Select("Username").
|
||||
From("ChannelMembers").
|
||||
Join("Users ON Users.ID=ChannelMembers.UserID").
|
||||
Where(sq.Eq{"ChannelID": ID})
|
||||
|
||||
var sb strings.Builder
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first := true
|
||||
for rows.Next() {
|
||||
if first {
|
||||
sb.WriteString(", ")
|
||||
first = false
|
||||
}
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sb.WriteString(name)
|
||||
}
|
||||
return &model.Workspace{ID: ID, Title: sb.String()}, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("count(*)").
|
||||
From("ChannelMembers").
|
||||
Where(sq.Eq{"ChannelID": workspaceID}).
|
||||
Where(sq.Eq{"UserID": userID})
|
||||
|
||||
row := query.QueryRow()
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
|
||||
builder := sq.StatementBuilder
|
||||
if s.dbType == postgresDBType || s.dbType == sqliteDBType {
|
||||
builder = builder.PlaceholderFormat(sq.Dollar)
|
||||
}
|
||||
|
||||
return builder.RunWith(s.mmDB)
|
||||
}
|
@ -375,6 +375,21 @@ func (mr *MockStoreMockRecorder) GetWorkspace(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), arg0)
|
||||
}
|
||||
|
||||
// HasWorkspaceAccess mocks base method.
|
||||
func (m *MockStore) HasWorkspaceAccess(arg0, arg1 string) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HasWorkspaceAccess", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// HasWorkspaceAccess indicates an expected call of HasWorkspaceAccess.
|
||||
func (mr *MockStoreMockRecorder) HasWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasWorkspaceAccess", reflect.TypeOf((*MockStore)(nil).HasWorkspaceAccess), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertBlock mocks base method.
|
||||
func (m *MockStore) InsertBlock(arg0 store.Container, arg1 model.Block) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -2,6 +2,7 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/mysql"
|
||||
@ -66,6 +68,29 @@ func (pm *PrefixedMigration) ReadDown(version uint) (io.ReadCloser, string, erro
|
||||
return pm.executeTemplate(r, identifier)
|
||||
}
|
||||
|
||||
// migrations in MySQL need to run with the multiStatements flag
|
||||
// enabled, so this method creates a new connection ensuring that it's
|
||||
// enabled
|
||||
func (s *SQLStore) getMySQLMigrationConnection() (*sql.DB, error) {
|
||||
config, err := mysqldriver.ParseDSN(s.connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Params["multiStatements"] = "true"
|
||||
connectionString := config.FormatDSN()
|
||||
|
||||
db, err := sql.Open(s.dbType, connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) Migrate() error {
|
||||
var driver database.Driver
|
||||
var err error
|
||||
@ -86,7 +111,13 @@ func (s *SQLStore) Migrate() error {
|
||||
}
|
||||
|
||||
if s.dbType == mysqlDBType {
|
||||
driver, err = mysql.WithInstance(s.db, &mysql.Config{MigrationsTable: migrationsTable})
|
||||
db, err := s.getMySQLMigrationConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
driver, err = mysql.WithInstance(db, &mysql.Config{MigrationsTable: migrationsTable})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ type SQLStore struct {
|
||||
db *sql.DB
|
||||
dbType string
|
||||
tablePrefix string
|
||||
connectionString string
|
||||
}
|
||||
|
||||
// New creates a new SQL implementation of the store.
|
||||
@ -40,9 +41,10 @@ func New(dbType, connectionString string, tablePrefix string) (*SQLStore, error)
|
||||
}
|
||||
|
||||
store := &SQLStore{
|
||||
db: db,
|
||||
dbType: dbType,
|
||||
tablePrefix: tablePrefix,
|
||||
db: db,
|
||||
dbType: dbType,
|
||||
tablePrefix: tablePrefix,
|
||||
connectionString: connectionString,
|
||||
}
|
||||
|
||||
err = store.Migrate()
|
||||
|
@ -104,3 +104,7 @@ func (s *SQLStore) GetWorkspace(ID string) (*model.Workspace, error) {
|
||||
|
||||
return &workspace, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) HasWorkspaceAccess(userID string, workspaceID string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -51,4 +51,5 @@ type Store interface {
|
||||
UpsertWorkspaceSignupToken(workspace model.Workspace) error
|
||||
UpsertWorkspaceSettings(workspace model.Workspace) error
|
||||
GetWorkspace(ID string) (*model.Workspace, error)
|
||||
HasWorkspaceAccess(userID string, workspaceID string) (bool, error)
|
||||
}
|
||||
|
@ -94,6 +94,10 @@ func (ws *Server) registerRoutes() {
|
||||
// Start runs the web server and start listening for charsetnnections.
|
||||
func (ws *Server) Start() {
|
||||
ws.registerRoutes()
|
||||
if ws.port == -1 {
|
||||
log.Print("server not bind to any port\n")
|
||||
return
|
||||
}
|
||||
|
||||
isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem")
|
||||
if isSSL {
|
||||
|
@ -15,21 +15,23 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
type WorkspaceAuthenticator interface {
|
||||
DoesUserHaveWorkspaceAccess(session *model.Session, workspaceID string) bool
|
||||
}
|
||||
|
||||
// IsValidSessionToken authenticates session tokens
|
||||
type IsValidSessionToken func(token string) bool
|
||||
|
||||
type Hub interface {
|
||||
SendWSMessage(data []byte)
|
||||
SetReceiveWSMessage(func(data []byte))
|
||||
}
|
||||
|
||||
// Server is a WebSocket server.
|
||||
type Server struct {
|
||||
upgrader websocket.Upgrader
|
||||
listeners map[string][]*websocket.Conn
|
||||
mu sync.RWMutex
|
||||
auth *auth.Auth
|
||||
singleUserToken string
|
||||
WorkspaceAuthenticator WorkspaceAuthenticator
|
||||
upgrader websocket.Upgrader
|
||||
listeners map[string][]*websocket.Conn
|
||||
mu sync.RWMutex
|
||||
auth *auth.Auth
|
||||
hub Hub
|
||||
singleUserToken string
|
||||
isMattermostAuth bool
|
||||
}
|
||||
|
||||
// UpdateMsg is sent on block updates
|
||||
@ -38,6 +40,13 @@ type UpdateMsg struct {
|
||||
Block model.Block `json:"block"`
|
||||
}
|
||||
|
||||
// clusterUpdateMsg is sent on block updates
|
||||
type clusterUpdateMsg struct {
|
||||
UpdateMsg
|
||||
BlockID string `json:"block_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
}
|
||||
|
||||
// ErrorMsg is sent on errors
|
||||
type ErrorMsg struct {
|
||||
Error string `json:"error"`
|
||||
@ -59,7 +68,7 @@ type websocketSession struct {
|
||||
}
|
||||
|
||||
// NewServer creates a new Server.
|
||||
func NewServer(auth *auth.Auth, singleUserToken string) *Server {
|
||||
func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool) *Server {
|
||||
return &Server{
|
||||
listeners: make(map[string][]*websocket.Conn),
|
||||
upgrader: websocket.Upgrader{
|
||||
@ -67,8 +76,9 @@ func NewServer(auth *auth.Auth, singleUserToken string) *Server {
|
||||
return true
|
||||
},
|
||||
},
|
||||
auth: auth,
|
||||
singleUserToken: singleUserToken,
|
||||
auth: auth,
|
||||
singleUserToken: singleUserToken,
|
||||
isMattermostAuth: isMattermostAuth,
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,10 +95,6 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Auth
|
||||
|
||||
log.Printf("CONNECT WebSocket onChange, client: %s", client.RemoteAddr())
|
||||
|
||||
// Make sure we close the connection when the function returns
|
||||
defer func() {
|
||||
log.Printf("DISCONNECT WebSocket onChange, client: %s", client.RemoteAddr())
|
||||
@ -99,9 +105,14 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
|
||||
client.Close()
|
||||
}()
|
||||
|
||||
userID := ""
|
||||
if ws.isMattermostAuth {
|
||||
userID = r.Header.Get("Mattermost-User-Id")
|
||||
}
|
||||
|
||||
wsSession := websocketSession{
|
||||
client: client,
|
||||
isAuthenticated: false,
|
||||
isAuthenticated: userID != "",
|
||||
}
|
||||
|
||||
// Simple message handling loop
|
||||
@ -124,19 +135,25 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request
|
||||
continue
|
||||
}
|
||||
|
||||
if userID != "" {
|
||||
if ws.auth.DoesUserHaveWorkspaceAccess(userID, command.WorkspaceID) {
|
||||
wsSession.workspaceID = command.WorkspaceID
|
||||
} else {
|
||||
log.Printf(`ERROR User doesn't have permissions to read the workspace: %s`, command.WorkspaceID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch command.Action {
|
||||
case "AUTH":
|
||||
log.Printf(`Command: AUTH, client: %s`, client.RemoteAddr())
|
||||
ws.authenticateListener(&wsSession, command.WorkspaceID, command.Token)
|
||||
|
||||
case "ADD":
|
||||
log.Printf(`Command: Add workspaceID: %s, blockIDs: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr())
|
||||
ws.addListener(&wsSession, &command)
|
||||
|
||||
case "REMOVE":
|
||||
log.Printf(`Command: Remove workspaceID: %s, blockID: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr())
|
||||
ws.removeListenerFromBlocks(&wsSession, &command)
|
||||
|
||||
default:
|
||||
log.Printf(`ERROR webSocket command, invalid action: %v`, command.Action)
|
||||
}
|
||||
@ -154,13 +171,7 @@ func (ws *Server) isValidSessionToken(token, workspaceID string) bool {
|
||||
}
|
||||
|
||||
// Check workspace permission
|
||||
if ws.WorkspaceAuthenticator != nil {
|
||||
if !ws.WorkspaceAuthenticator.DoesUserHaveWorkspaceAccess(session, workspaceID) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return ws.auth.DoesUserHaveWorkspaceAccess(session.UserID, workspaceID)
|
||||
}
|
||||
|
||||
func (ws *Server) authenticateListener(wsSession *websocketSession, workspaceID, token string) {
|
||||
@ -301,6 +312,38 @@ func sendError(conn *websocket.Conn, message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) SetHub(hub Hub) {
|
||||
ws.hub = hub
|
||||
ws.hub.SetReceiveWSMessage(func(data []byte) {
|
||||
var msg clusterUpdateMsg
|
||||
err := json.Unmarshal(data, &msg)
|
||||
if err != nil {
|
||||
log.Printf("unable to unmarshal cluster message")
|
||||
return
|
||||
}
|
||||
|
||||
listeners := ws.getListeners(msg.WorkspaceID, msg.BlockID)
|
||||
log.Printf("%d listener(s) for blockID: %s", len(listeners), msg.BlockID)
|
||||
|
||||
message := UpdateMsg{
|
||||
Action: msg.Action,
|
||||
Block: msg.Block,
|
||||
}
|
||||
|
||||
if listeners != nil {
|
||||
for _, listener := range listeners {
|
||||
log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", msg.WorkspaceID, msg.BlockID, listener.RemoteAddr())
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
log.Printf("broadcast error: %v", err)
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getListeners returns the listeners to a blockID's changes.
|
||||
func (ws *Server) getListeners(workspaceID string, blockID string) []*websocket.Conn {
|
||||
ws.mu.Lock()
|
||||
@ -331,12 +374,19 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) {
|
||||
listeners := ws.getListeners(workspaceID, blockID)
|
||||
log.Printf("%d listener(s) for blockID: %s", len(listeners), blockID)
|
||||
|
||||
if listeners != nil {
|
||||
message := UpdateMsg{
|
||||
Action: "UPDATE_BLOCK",
|
||||
Block: block,
|
||||
message := UpdateMsg{
|
||||
Action: "UPDATE_BLOCK",
|
||||
Block: block,
|
||||
}
|
||||
if ws.hub != nil {
|
||||
data, err := json.Marshal(clusterUpdateMsg{UpdateMsg: message, WorkspaceID: workspaceID, BlockID: blockID})
|
||||
if err != nil {
|
||||
log.Printf("unable to serialize websocket message %v with the error: %v", message, err)
|
||||
}
|
||||
ws.hub.SendWSMessage(data)
|
||||
}
|
||||
|
||||
if listeners != nil {
|
||||
for _, listener := range listeners {
|
||||
log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", workspaceID, blockID, listener.RemoteAddr())
|
||||
|
||||
|
@ -66,7 +66,7 @@ class OctoListener {
|
||||
|
||||
const url = new URL(this.serverUrl)
|
||||
const protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:'
|
||||
const wsServerUrl = `${protocol}//${url.host}${url.pathname}ws/onchange`
|
||||
const wsServerUrl = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}/ws/onchange`
|
||||
Utils.log(`OctoListener open: ${wsServerUrl}`)
|
||||
const ws = new WebSocket(wsServerUrl)
|
||||
this.ws = ws
|
||||
|
Loading…
x
Reference in New Issue
Block a user