From 401c9bf7435003469fd9d13799cd76edd436c468 Mon Sep 17 00:00:00 2001 From: Jim Wang Date: Mon, 23 Nov 2020 17:08:56 -0700 Subject: [PATCH] feat(security): Add new implementation for security bootstrapping/installation Closes: #2884 The scurity container bootstrapping initiates with security-bootstrapper service, in which it bootstraps the pre-seeded secrets and credentials. The security-bootstrapper starts with seeding the vault_wait_install.sh script (contains the dockerize utility) to be available for other containers that needs to wait for the intended done-listener is issued and connected. The other containers in the security bootstrapping process currently are: - Redis bootstrapping - Consul bootstrapping - Kong bootstrapping - Postgres bootstrapping The dockerize utility is used on those above containers to wait for that security-bootstrapper tcp listener done signal and then those container can proceed to start up. Security-bootstrapper's entrypoint script is also for other edgex-core-services to wait for the intended port to be ready. The majority of edgex-core-services are converted to alpine-based image to facilitate the ability to use entrypoint scripts. Also remove unused environment flag: SECRETSTORE_SETUP_DONE_FLAG from secretstore_setup's entrypoint script That env flag will also needed to be cleaned up on the docker-compose file All timeout for dockerize wait-timeout in one central place. Also add the consul local config for disabling the auto check upate as currently consul is not running in SSL mode. Add lib/pq into Attribution.txt Run security-bootstrapper executeable as non-root user, $$EDGEX_USER All env. variables of entrypoint scripts are populated from the env files of compose builder in developer-scripts Signed-off-by: Jim Wang --- .gitignore | 1 + Attribution.txt | 3 + Makefile | 20 ++- SECURITY.md | 4 +- cmd/core-command/Dockerfile | 6 +- cmd/core-data/Dockerfile | 2 +- cmd/core-metadata/Dockerfile | 6 +- cmd/security-bootstrap-redis/Dockerfile | 53 ------ cmd/security-bootstrap-redis/entrypoint.sh | 27 --- cmd/security-bootstrapper/Dockerfile | 83 +++++++++ .../entrypoint-scripts/consul_wait_install.sh | 80 +++++++++ .../entrypoint-scripts/kong_wait_install.sh | 80 +++++++++ .../postgres_wait_install.sh | 99 +++++++++++ .../proxy_setup_wait_install.sh | 56 ++++++ .../ready_to_run_wait_install.sh | 40 +++++ .../entrypoint-scripts/redis_wait_install.sh | 62 +++++++ .../entrypoint-scripts/vault_wait_install.sh | 57 +++++++ cmd/security-bootstrapper/entrypoint.sh | 49 ++++++ cmd/security-bootstrapper/main.go | 27 +++ .../res/configuration.toml | 27 +++ cmd/security-secretstore-setup/entrypoint.sh | 39 ++--- cmd/support-notifications/Dockerfile | 9 +- cmd/support-scheduler/Dockerfile | 8 +- go.mod | 1 + .../bootstrapper/command/cmd_dispatcher.go | 69 ++++++++ .../command/cmd_dispatcher_test.go | 71 ++++++++ .../bootstrapper/command/flags_common.go | 107 ++++++++++++ .../bootstrapper/command/gate/command.go | 150 ++++++++++++++++ .../bootstrapper/command/gate/command_test.go | 160 ++++++++++++++++++ .../command/genpassword/command.go | 91 ++++++++++ .../command/genpassword/command_test.go | 75 ++++++++ .../command/gethttpstatus/command.go | 113 +++++++++++++ .../command/gethttpstatus/command_test.go | 101 +++++++++++ .../bootstrapper/command/help/command.go | 69 ++++++++ .../bootstrapper/command/help/command_test.go | 58 +++++++ .../bootstrapper/command/listen/command.go | 96 +++++++++++ .../command/listen/command_test.go | 136 +++++++++++++++ .../bootstrapper/command/ping/command.go | 134 +++++++++++++++ .../bootstrapper/command/ping/command_test.go | 57 +++++++ .../security/bootstrapper/config/config.go | 84 +++++++++ .../security/bootstrapper/config/types.go | 80 +++++++++ .../bootstrapper/container/container.go | 30 ++++ .../security/bootstrapper/handlers/init.go | 102 +++++++++++ .../bootstrapper/interfaces/command.go | 31 ++++ internal/security/bootstrapper/main.go | 76 +++++++++ internal/security/bootstrapper/tcp/client.go | 67 ++++++++ .../security/bootstrapper/tcp/client_test.go | 90 ++++++++++ .../security/bootstrapper/tcp/listener.go | 92 ++++++++++ .../bootstrapper/tcp/listener_test.go | 100 +++++++++++ internal/security/redis/main.go | 9 +- 50 files changed, 2960 insertions(+), 127 deletions(-) delete mode 100644 cmd/security-bootstrap-redis/Dockerfile delete mode 100644 cmd/security-bootstrap-redis/entrypoint.sh create mode 100644 cmd/security-bootstrapper/Dockerfile create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/consul_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/kong_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/proxy_setup_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/ready_to_run_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint-scripts/vault_wait_install.sh create mode 100755 cmd/security-bootstrapper/entrypoint.sh create mode 100644 cmd/security-bootstrapper/main.go create mode 100644 cmd/security-bootstrapper/res/configuration.toml create mode 100644 internal/security/bootstrapper/command/cmd_dispatcher.go create mode 100644 internal/security/bootstrapper/command/cmd_dispatcher_test.go create mode 100644 internal/security/bootstrapper/command/flags_common.go create mode 100644 internal/security/bootstrapper/command/gate/command.go create mode 100644 internal/security/bootstrapper/command/gate/command_test.go create mode 100644 internal/security/bootstrapper/command/genpassword/command.go create mode 100644 internal/security/bootstrapper/command/genpassword/command_test.go create mode 100644 internal/security/bootstrapper/command/gethttpstatus/command.go create mode 100644 internal/security/bootstrapper/command/gethttpstatus/command_test.go create mode 100644 internal/security/bootstrapper/command/help/command.go create mode 100644 internal/security/bootstrapper/command/help/command_test.go create mode 100644 internal/security/bootstrapper/command/listen/command.go create mode 100644 internal/security/bootstrapper/command/listen/command_test.go create mode 100644 internal/security/bootstrapper/command/ping/command.go create mode 100644 internal/security/bootstrapper/command/ping/command_test.go create mode 100644 internal/security/bootstrapper/config/config.go create mode 100644 internal/security/bootstrapper/config/types.go create mode 100644 internal/security/bootstrapper/container/container.go create mode 100644 internal/security/bootstrapper/handlers/init.go create mode 100644 internal/security/bootstrapper/interfaces/command.go create mode 100644 internal/security/bootstrapper/main.go create mode 100644 internal/security/bootstrapper/tcp/client.go create mode 100644 internal/security/bootstrapper/tcp/client_test.go create mode 100644 internal/security/bootstrapper/tcp/listener.go create mode 100644 internal/security/bootstrapper/tcp/listener_test.go diff --git a/.gitignore b/.gitignore index 656cb8c780..4bfb1742bd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ cmd/sys-mgmt-agent/sys-mgmt-agent cmd/sys-mgmt-executor/sys-mgmt-executor cmd/security-bootstrap-redis/security-bootstrap-redis cmd/secrets-config/secrets-config +cmd/security-bootstrapper/security-bootstrapper docs/_build/ diff --git a/Attribution.txt b/Attribution.txt index 7d1044ef5a..d293883ca3 100644 --- a/Attribution.txt +++ b/Attribution.txt @@ -175,3 +175,6 @@ https://github.com/go-playground/validator/blob/master/LICENSE leodido/go-urn (MIT) https://github.com/leodido/go-urn https://github.com/leodido/go-urn + +github.com/lib/pq (MIT) https://github.com/lib/pq +https://github.com/lib/pq/blob/master/LICENSE.md diff --git a/Makefile b/Makefile index baf814d0aa..427467ec97 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ DOCKERS= \ docker_support_scheduler \ docker_security_proxy_setup \ docker_security_secretstore_setup \ - docker_security_bootstrap_redis + docker_security_bootstrapper .PHONY: $(DOCKERS) @@ -35,7 +35,8 @@ MICROSERVICES= \ cmd/security-secretstore-setup/security-secretstore-setup \ cmd/security-file-token-provider/security-file-token-provider \ cmd/security-bootstrap-redis/security-bootstrap-redis \ - cmd/secrets-config/secrets-config + cmd/secrets-config/secrets-config \ + cmd/security-bootstrapper/security-bootstrapper .PHONY: $(MICROSERVICES) @@ -87,6 +88,9 @@ cmd/security-bootstrap-redis/security-bootstrap-redis: cmd/secrets-config/secrets-config: $(GO) build $(GOFLAGS) -o ./cmd/secrets-config ./cmd/secrets-config +cmd/security-bootstrapper/security-bootstrapper: + $(GO) build $(GOFLAGS) -o ./cmd/security-bootstrapper/security-bootstrapper ./cmd/security-bootstrapper + clean: rm -f $(MICROSERVICES) @@ -186,10 +190,12 @@ docker_security_secretstore_setup: -t edgexfoundry/docker-security-secretstore-setup-go:$(DOCKER_TAG) \ . -docker_security_bootstrap_redis: - docker build \ - -f cmd/security-bootstrap-redis/Dockerfile \ +docker_security_bootstrapper: + docker build \ + --build-arg http_proxy \ + --build-arg https_proxy \ + -f cmd/security-bootstrapper/Dockerfile \ --label "git_sha=$(GIT_SHA)" \ - -t edgexfoundry/docker-security-bootstrap-redis-go:$(GIT_SHA) \ - -t edgexfoundry/docker-security-bootstrap-redis-go:$(DOCKER_TAG) \ + -t edgexfoundry/docker-security-bootstrapper-go:$(GIT_SHA) \ + -t edgexfoundry/docker-security-bootstrapper-go:$(DOCKER_TAG) \ . diff --git a/SECURITY.md b/SECURITY.md index 9b0caa9e21..6c95abcc6c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,8 +13,8 @@ There are 2 major components that are responsible for security features: When starting a secure EdgeX deployment, the sequence is [see docker-compose-nexus-redis.yml for reference](https://github.com/edgexfoundry/developer-scripts/blob/master/releases/nightly-build/compose-files/docker-compose-nexus-redis.yml)) 1. Start [Vault by HashiCorp](https://www.vaultproject.io/) -1. Start the `edgex-vault-worker` container from the `docker-edgex-security-secretstore-setup-go` image to create the shared secrets needed by the microservices. -1. Finally, the start the `edgex-proxy` container from the `docker-edgex-security-proxy-setup-go` image once [Kong](https://konghq.com/) is up. +1. Start the `edgex-secretstore-setup` container from the `docker-edgex-security-secretstore-setup-go` image to create the shared secrets needed by the microservices. +1. Finally, the start the `edgex-proxy-setup` container from the `docker-edgex-security-proxy-setup-go` image once [Kong](https://konghq.com/) is up. ## Get Started diff --git a/cmd/core-command/Dockerfile b/cmd/core-command/Dockerfile index 61a072af09..a1dbfeb905 100644 --- a/cmd/core-command/Dockerfile +++ b/cmd/core-command/Dockerfile @@ -36,10 +36,12 @@ COPY . . RUN make cmd/core-command/core-command -FROM scratch +FROM alpine:3.12 + +RUN apk add --update --no-cache dumb-init LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2018: Dell, Cavium' + copyright='Copyright (c) 2018: Dell, Cavium, Copyright (c) 2021: Intel Corporation' ENV APP_PORT=48082 #expose command data port diff --git a/cmd/core-data/Dockerfile b/cmd/core-data/Dockerfile index f91b551a74..820f57b061 100644 --- a/cmd/core-data/Dockerfile +++ b/cmd/core-data/Dockerfile @@ -50,7 +50,7 @@ EXPOSE $APP_PORT # So we can try these. RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories -RUN apk add --update --no-cache zeromq +RUN apk add --update --no-cache zeromq dumb-init COPY --from=builder /edgex-go/Attribution.txt / COPY --from=builder /edgex-go/cmd/core-data/core-data / COPY --from=builder /edgex-go/cmd/core-data/res/configuration.toml /res/configuration.toml diff --git a/cmd/core-metadata/Dockerfile b/cmd/core-metadata/Dockerfile index 0afe3cd8e7..5caf714563 100644 --- a/cmd/core-metadata/Dockerfile +++ b/cmd/core-metadata/Dockerfile @@ -36,10 +36,12 @@ COPY . . RUN make cmd/core-metadata/core-metadata #Next image - Copy built Go binary into new workspace -FROM scratch +FROM alpine:3.12 + +RUN apk add --update --no-cache dumb-init LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2018: Dell, Cavium' + copyright='Copyright (c) 2018: Dell, Cavium, Copyright (c) 2021: Intel Corporation' ENV APP_PORT=48081 #expose meta data port diff --git a/cmd/security-bootstrap-redis/Dockerfile b/cmd/security-bootstrap-redis/Dockerfile deleted file mode 100644 index 40aa5c066a..0000000000 --- a/cmd/security-bootstrap-redis/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -# ---------------------------------------------------------------------------------- -# Copyright 2020 Redis Labs -# -# 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. -# -# @author: Andre Srinivasan -# ---------------------------------------------------------------------------------- - -ARG BUILDER_BASE=golang:1.15-alpine3.12 -FROM ${BUILDER_BASE} AS builder - -WORKDIR /edgex-go - -# The main mirrors are giving us timeout issues on builds periodically. -# So we can try these. - -RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories - -RUN apk add --update --no-cache make git - -COPY go.mod . - -RUN go mod download - -COPY . . - -RUN make cmd/security-bootstrap-redis/security-bootstrap-redis - -FROM alpine:3.12 - -RUN apk add --update --no-cache ca-certificates dumb-init curl - -LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2020 Redis Labs' - -WORKDIR / -COPY --from=builder /edgex-go/Attribution.txt / -COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/security-bootstrap-redis / -COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/entrypoint.sh / -COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/res/configuration.toml /res/configuration.toml -RUN chmod +x entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/cmd/security-bootstrap-redis/entrypoint.sh b/cmd/security-bootstrap-redis/entrypoint.sh deleted file mode 100644 index 7e974c84f4..0000000000 --- a/cmd/security-bootstrap-redis/entrypoint.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/dumb-init /bin/sh -# ---------------------------------------------------------------------------------- -# Copyright (c) 2020 Redis Labs -# -# 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. -# -# SPDX-License-Identifier: Apache-2.0' -# ---------------------------------------------------------------------------------- - -set -e - -echo "Starting security-bootstrap-redis..." - -/security-bootstrap-redis - -echo "Waiting for termination signal" -exec tail -f /dev/null diff --git a/cmd/security-bootstrapper/Dockerfile b/cmd/security-bootstrapper/Dockerfile new file mode 100644 index 0000000000..8871af68c0 --- /dev/null +++ b/cmd/security-bootstrapper/Dockerfile @@ -0,0 +1,83 @@ +# ---------------------------------------------------------------------------------- +# Copyright 2021 Intel Corp. +# +# 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. +# +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +ARG BUILDER_BASE=golang:1.15-alpine3.12 +FROM ${BUILDER_BASE} AS builder + +WORKDIR /edgex-go + +RUN sed -e 's/dl-cdn[.]alpinelinux.org/nl.alpinelinux.org/g' -i~ /etc/apk/repositories + +RUN apk add --update --no-cache make git + +COPY go.mod . + +RUN go mod download + +COPY . . + +RUN make cmd/security-bootstrapper/security-bootstrapper \ + && make cmd/security-bootstrap-redis/security-bootstrap-redis + +FROM alpine:3.12 + +RUN apk add --update --no-cache dumb-init openssl su-exec + +LABEL license='SPDX-License-Identifier: Apache-2.0' \ + copyright='Copyright (c) 2021 Intel Corporation' + +# Use dockerize utility for services to wait for certain ports to be available +ENV DOCKERIZE_VERSION v0.6.1 +RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz + +ENV SECURITY_INIT_DIR /edgex-init +ARG BOOTSTRAP_REDIS_DIR=${SECURITY_INIT_DIR}/bootstrap-redis + +RUN mkdir -p ${SECURITY_INIT_DIR} \ + && mkdir -p ${BOOTSTRAP_REDIS_DIR} \ + && echo "Move dockerize executable" \ + && mv /usr/local/bin/dockerize ${SECURITY_INIT_DIR} + +WORKDIR ${SECURITY_INIT_DIR} + +# copy all entrypoint scripts into shared folder +COPY --from=builder /edgex-go/cmd/security-bootstrapper/entrypoint-scripts/ ${SECURITY_INIT_DIR}/ +RUN chmod +x ${SECURITY_INIT_DIR}/*.sh + +COPY --from=builder /edgex-go/Attribution.txt / +COPY --from=builder /edgex-go/cmd/security-bootstrapper/security-bootstrapper . +COPY --from=builder /edgex-go/cmd/security-bootstrapper/res/configuration.toml ./res/ + +# needed for bootstrapping Redis db +COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/security-bootstrap-redis ${BOOTSTRAP_REDIS_DIR}/ +COPY --from=builder /edgex-go/cmd/security-bootstrap-redis/res/configuration.toml ${BOOTSTRAP_REDIS_DIR}/res/ + +# Expose the file directory as a volume since there's long-running state +VOLUME ${SECURITY_INIT_DIR} + +# setup entry point script +COPY --from=builder /edgex-go/cmd/security-bootstrapper/entrypoint.sh / +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +# gate is one subcommand for security-bootstrapper to do security bootstrapping +CMD ["gate"] diff --git a/cmd/security-bootstrapper/entrypoint-scripts/consul_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/consul_wait_install.sh new file mode 100755 index 0000000000..bcb57552c8 --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/consul_wait_install.sh @@ -0,0 +1,80 @@ +#!/usr/bin/dumb-init /bin/sh +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for Consul and run on the consul's container +# In particular, it waits for Vault to be ready to roll + +set -e + +# function to check on Vault for readiness +vault_ready() +{ + vault_host=$1 + vault_port=$2 + resp_code=$(curl --write-out '%{http_code}' --silent --output /dev/null "${vault_host}":"${vault_port}"/v1/sys/health) + if [ "$resp_code" -eq 200 ] ; then + echo 1 + else + echo 0 + fi +} + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on Consul" + +echo "$(date) Consul waits on Vault to be initialized" +# check the http status code from Vault using SECRETSTORE_HOST and SECRETSTORE_PORT as input to the function call +vault_inited=$(vault_ready "${SECRETSTORE_HOST}" "${SECRETSTORE_PORT}") +until [ "$vault_inited" -eq 1 ]; do + echo "$(date) waiting for Vault ${SECRETSTORE_HOST}:${SECRETSTORE_PORT} to be initialized"; + sleep 1; + vault_inited=$(vault_ready "${SECRETSTORE_HOST}" "${SECRETSTORE_PORT}") +done + +# only in json format according to Consul's documentation +DEFAULT_CONSUL_LOCAL_CONFIG=' +{ + "enable_local_script_checks": true, + "disable_update_check": true +} +' + +# set the default value to environment var if not present +CONSUL_LOCAL_CONFIG=${CONSUL_LOCAL_CONFIG:-$DEFAULT_CONSUL_LOCAL_CONFIG} + +export CONSUL_LOCAL_CONFIG + +echo "$(date) CONSUL_LOCAL_CONFIG: ${CONSUL_LOCAL_CONFIG}" + +echo "$(date) Starting edgex-consul..." +exec docker-entrypoint.sh agent -ui -bootstrap -server -client 0.0.0.0 & + +# wait for the consul port +echo "$(date) Executing dockerize on Consul with waiting on its own port \ + tcp://${STAGEGATE_REGISTRY_HOST}:${STAGEGATE_REGISTRY_PORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_REGISTRY_HOST}":"${STAGEGATE_REGISTRY_PORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +# Signal that Consul is ready for services blocked waiting on Consul +/edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \ + --port="${STAGEGATE_REGISTRY_READYPORT}" --host="${STAGEGATE_REGISTRY_HOST}" +if [ $? -ne 0 ]; then + echo "$(date) failed to gating the consul ready port, exits" +fi diff --git a/cmd/security-bootstrapper/entrypoint-scripts/kong_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/kong_wait_install.sh new file mode 100755 index 0000000000..44eb6522cc --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/kong_wait_install.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for Kong. +# In particular, it waits for the security-bootstrapper's ReadyToRunPort and Postgres db ready to roll + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on Kong" + +# gating on the ready-to-run port +echo "$(date) Executing dockerize with waiting on tcp://${STAGEGATE_BOOTSTRAPPER_HOST}:${STAGEGATE_READY_TORUNPORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_BOOTSTRAPPER_HOST}":"${STAGEGATE_READY_TORUNPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +echo "$(date) Kong waits on Postgres to be initialized" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_KONGDB_HOST}":"${STAGEGATE_KONGDB_READYPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +# KONG_PG_PASSWORD_FILE is env used by Kong, it is for kong-db's password file +echo "$(date) Executing dockerize with waiting on file:${KONG_PG_PASSWORD_FILE}" +/edgex-init/dockerize -wait file://"${KONG_PG_PASSWORD_FILE}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +# double check and make sure the postgres is setup with that password and ready +passwd=$(cat "${KONG_PG_PASSWORD_FILE}") +pg_inited=0 +until [ $pg_inited -eq 1 ]; do + status=$(/edgex-init/security-bootstrapper --confdir=/edgex-init/res pingPgDb \ + --username=kong --dbname=kong --password="${passwd}" | tail -n 1) + if [ ${#status} -gt 0 ] && [[ "${status}" != *ERROR* ]]; then + if [ "${status}" = "ready" ]; then + pg_inited=1 + passwd="" + fi + fi + if [ $pg_inited -ne 1 ]; then + echo "$(date) waiting for ${STAGEGATE_KONGDB_HOST} to be initialized" + sleep 1 + fi +done + +echo "$(date) Check point: postgres db is ready for kong" + +# in kong's docker, we use KONG_PG_PASSWORD_FILE instead of KONG_PG_PASSWORD for better security +export KONG_PG_PASSWORD_FILE + +# remove env KONG_PG_PASSWORD: only use KONG_PG_PASSWORD_FILE +unset KONG_PG_PASSWORD + +/docker-entrypoint.sh kong migrations bootstrap +/docker-entrypoint.sh kong migrations list +code=$? +if [ $code -eq 0 ]; then + echo "$(date) kong migrations bootstrap ok, doing migrations up and finish..." + /docker-entrypoint.sh kong migrations up && /docker-entrypoint.sh kong migrations finish +else + echo "$(date) failed to kong migrations, returned code = " $code +fi + +echo "$(date) Starting kong ..." +exec /docker-entrypoint.sh kong docker-start diff --git a/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh new file mode 100755 index 0000000000..93ff856f6a --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/postgres_wait_install.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for Postgres. +# In particular, it waits for Vault to be ready and dynamically generated password seeded into db + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on Postgres" + +# Postgres is waiting for BOOTSTRAP_PORT +echo "$(date) Executing dockerize on Postgres with waiting on \ + tcp://${STAGEGATE_BOOTSTRAPPER_HOST}:${STAGEGATE_BOOTSTRAPPER_STARTPORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_BOOTSTRAPPER_HOST}":"${STAGEGATE_BOOTSTRAPPER_STARTPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +echo "$(date) Postgres waits on Vault to be initialized" + +vault_inited=0 +until [ $vault_inited -eq 1 ]; do + status=$(/edgex-init/security-bootstrapper --confdir=/edgex-init/res getHttpStatus \ + --url=http://"${SECRETSTORE_HOST}":"${SECRETSTORE_PORT}"/v1/sys/health | tail -n 1) + if [ ${#status} -gt 0 ] && [[ "${status}" != *ERROR* ]]; then + echo "$(date) ${SECRETSTORE_HOST}:${SECRETSTORE_PORT} status code = ${status}" + if [ "$status" -eq 200 ]; then + vault_inited=1 + fi + fi + if [ $vault_inited -ne 1 ]; then + echo "$(date) waiting for ${SECRETSTORE_HOST} to be initialized" + sleep 1 + fi +done + +echo "$(date) ${SECRETSTORE_HOST} is ready" + +# POSTGRES_PASSWORD_FILE env is used by Postgres and it is for the db password file +# if password already in then re-use +if [ -n "${POSTGRES_PASSWORD_FILE}" ] && [ -f "${POSTGRES_PASSWORD_FILE}" ]; then + echo "$(date) previous file already exists, skipping creation" +else + # create password file for postgres to be used in the compose file + mkdir -p "$(dirname "${POSTGRES_PASSWORD_FILE}")" + out=$(/edgex-init/security-bootstrapper --confdir=/edgex-init/res genPassword | tail -n 1) + if [ ${#out} -gt 0 ] && [[ "${out}" != *ERROR* ]]; then + echo "${out}" > "${POSTGRES_PASSWORD_FILE}" + fi +fi + +chmod 444 "${POSTGRES_PASSWORD_FILE}" +export POSTGRES_PASSWORD_FILE + +echo "$(date) Starting kong-db..." +exec /usr/local/bin/docker-entrypoint.sh postgres & + +# check that the postgres is initialized +passwd=$(cat "${POSTGRES_PASSWORD_FILE}") +pg_inited=0 +until [ $pg_inited -eq 1 ]; do + status=$(/edgex-init/security-bootstrapper --confdir=/edgex-init/res pingPgDb \ + --username=kong --dbname=kong --password="${passwd}" | tail -n 1) + if [ ${#status} -gt 0 ] && [[ "${status}" != *ERROR* ]]; then + if [ "${status}" = "ready" ]; then + pg_inited=1 + passwd="" + fi + fi + if [ $pg_inited -ne 1 ]; then + echo "$(date) waiting for ${STAGEGATE_KONGDB_HOST} to be initialized" + sleep 1 + fi +done + +echo "$(date) ${STAGEGATE_KONGDB_HOST} is initialized" + +# Signal that Postgres is ready for services blocked waiting on Postgres +/edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \ + --port="${STAGEGATE_KONGDB_READYPORT}" --host="${STAGEGATE_KONGDB_HOST}" +if [ $? -ne 0 ]; then + echo "$(date) failed to gating the postgres ready port, exits" +fi diff --git a/cmd/security-bootstrapper/entrypoint-scripts/proxy_setup_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/proxy_setup_wait_install.sh new file mode 100755 index 0000000000..aa97567c7f --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/proxy_setup_wait_install.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for proxy-setup. +# In particular, it waits for ready-to-run port and kong to be ready + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on proxy-setup" + +# gating on the ready-to-run port +echo "$(date) Executing dockerize for ${PROXY_SETUP_HOST} with waiting on \ + tcp://${STAGEGATE_BOOTSTRAPPER_HOST}:${STAGEGATE_READY_TORUNPORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_BOOTSTRAPPER_HOST}":"${STAGEGATE_READY_TORUNPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +echo "$(date) ${PROXY_SETUP_HOST} waits on Kong to be initialized" + +kong_inited=0 +until [ $kong_inited -eq 1 ]; do + status=$(/edgex-init/security-bootstrapper --confdir=/edgex-init/res getHttpStatus \ + --url=http://"${API_GATEWAY_HOST}":"${API_GATEWAY_STATUS_PORT}"/status | tail -n 1) + if [ ${#status} -gt 0 ] && [[ "${status}" != *ERROR* ]]; then + echo "$(date) ${API_GATEWAY_HOST}:${API_GATEWAY_STATUS_PORT} status code = ${status}" + if [ "$status" -eq 200 ]; then + kong_inited=1 + fi + fi + if [ $kong_inited -ne 1 ]; then + echo "$(date) waiting for ${API_GATEWAY_HOST} to be initialized" + sleep 1 + fi +done + +echo "$(date) ${API_GATEWAY_HOST} is ready" + +echo "$(date) Starting ${PROXY_SETUP_HOST} ..." +exec /edgex/security-proxy-setup --init=true diff --git a/cmd/security-bootstrapper/entrypoint-scripts/ready_to_run_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/ready_to_run_wait_install.sh new file mode 100755 index 0000000000..9dc6c49907 --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/ready_to_run_wait_install.sh @@ -0,0 +1,40 @@ +#!/usr/bin/dumb-init /bin/sh +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for other Edgex services. +# In particular, it waits for the ReadyToRunPort raised to be ready to roll +# +# Note: +# Since the entrypoint script is overridden, user should also override the command +# so that the $@ is set appropriately on the run-time. +# + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting on security bootstrapping ready-to-run" + +# gating on the ready-to-run port +echo "$(date) Executing dockerize with $@ waiting on tcp://${STAGEGATE_BOOTSTRAPPER_HOST}:${STAGEGATE_READY_TORUNPORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_BOOTSTRAPPER_HOST}":"${STAGEGATE_READY_TORUNPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +echo "$(date) Starting $@ ..." +exec "$@" diff --git a/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh new file mode 100755 index 0000000000..60406813f1 --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/redis_wait_install.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for Redis. +# In particular, it waits for the TokensReady Port being ready to roll + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on Redis" + +# gating on the TokensReadyPort +echo "$(date) Executing dockerize on Redis with waiting on TokensReadyPort \ + tcp://${STAGEGATE_SECRETSTORESETUP_HOST}:${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}" +/edgex-init/dockerize -wait tcp://"${STAGEGATE_SECRETSTORESETUP_HOST}":"${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + +# the bootstrap-redis needs the connection from Redis db to set it up. +# Hence, here bootstrap-redis runs in background and then after bootstrap-redis starts, +# the Redis db starts in background. +echo "$(date) ${STAGEGATE_SECRETSTORESETUP_HOST} tokens ready, bootstrapping redis..." +/edgex-init/bootstrap-redis/security-bootstrap-redis --confdir=/edgex-init/bootstrap-redis/res & +redis_bootstrapper_pid=$! + +# give some time for bootstrap-redis to start up +sleep 1 + +echo "$(date) Starting edgex-redis..." +exec /usr/local/bin/docker-entrypoint.sh redis-server & + +# wait for bootstrap-redis to finish before signal the redis is ready +wait $redis_bootstrapper_pid +redis_bootstrapping_status=$? +if [ $redis_bootstrapping_status -eq 0 ]; then + echo "$(date) redis is bootstrapped and ready" +else + echo "$(date) failed to bootstrap redis" +fi + +# Signal that Redis is ready for services blocked waiting on Redis +/edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \ + --port="${STAGEGATE_DATABASE_READYPORT}" --host="${DATABASES_PRIMARY_HOST}" +if [ $? -ne 0 ]; then + echo "$(date) failed to gating the redis ready port, exits" +fi diff --git a/cmd/security-bootstrapper/entrypoint-scripts/vault_wait_install.sh b/cmd/security-bootstrapper/entrypoint-scripts/vault_wait_install.sh new file mode 100755 index 0000000000..d5dda4d317 --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint-scripts/vault_wait_install.sh @@ -0,0 +1,57 @@ +#!/usr/bin/dumb-init /bin/sh +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +# This is customized entrypoint script for Vault. +# In particular, it waits for the BootstrapPort ready to roll + +set -e + +# env settings are populated from env files of docker-compose + +echo "Script for waiting security bootstrapping on Vault" + +DEFAULT_VAULT_LOCAL_CONFIG=' +listener "tcp" { + address = "edgex-vault:8200" + tls_disable = "1" + cluster_address = "edgex-vault:8201" + } + backend "file" { + path = "/vault/file" + } + default_lease_ttl = "168h" + max_lease_ttl = "720h" +' + +VAULT_LOCAL_CONFIG=${VAULT_LOCAL_CONFIG:-$DEFAULT_VAULT_LOCAL_CONFIG} + +export VAULT_LOCAL_CONFIG + +echo "$(date) VAULT_LOCAL_CONFIG: ${VAULT_LOCAL_CONFIG}" + +if [ "$1" = 'server' ]; then + echo "$(date) Executing dockerize on vault $* with waiting on \ + tcp://${STAGEGATE_BOOTSTRAPPER_HOST}:${STAGEGATE_BOOTSTRAPPER_STARTPORT}" + /edgex-init/dockerize \ + -wait tcp://"${STAGEGATE_BOOTSTRAPPER_HOST}":"${STAGEGATE_BOOTSTRAPPER_STARTPORT}" \ + -timeout "${SECTY_BOOTSTRAP_GATING_TIMEOUT_DURATION}" + + echo "$(date) Starting edgex-vault..." + exec /usr/local/bin/docker-entrypoint.sh server -log-level=info +fi diff --git a/cmd/security-bootstrapper/entrypoint.sh b/cmd/security-bootstrapper/entrypoint.sh new file mode 100755 index 0000000000..4e29f0dd14 --- /dev/null +++ b/cmd/security-bootstrapper/entrypoint.sh @@ -0,0 +1,49 @@ +#!/usr/bin/dumb-init /bin/sh +# +# The entry point script uses dumb-init as the top-level process to reap any +# zombie processes +# +# ---------------------------------------------------------------------------------- +# Copyright (c) 2021 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ---------------------------------------------------------------------------------- + +set -e + +# Passing the arguments to the executable as $@ contains only the CMD arguments without the executable name +# treat anything not /bin/sh as to run this security-bootstrapper executable with the arguments +# this is useful for debugging the container like running with `docker run -it --rm security-bootstrapper /bin/sh` +if [ ! "$1" = '/bin/sh' ]; then + set -- security-bootstrapper "$@" +fi + +DEFAULT_EDGEX_USER_ID=2002 +EDGEX_USER_ID=${EDGEX_USER:-$DEFAULT_EDGEX_USER_ID} + +# During the bootstrapping, environment variables come for compose file environment files, +# which then injecting into all other related containers on other services' entrypoint scripts +# if the executable is not 'security-bootstrapper'; then we consider it not running the bootstrapping process +# for the user may just want to debug into the container shell itself +if [ "$1" = 'security-bootstrapper' ]; then + # run the executable as ${EDGEX_USER} + echo "$(date) Executing ./$@" + exec su-exec ${EDGEX_USER_ID} "./$@" + +else + # for debug purposes like docker run -it --rm security-bootstrapper:0.0.0-dev /bin/sh + echo "current directory:" "$PWD" + exec su-exec ${EDGEX_USER_ID} "$@" +fi diff --git a/cmd/security-bootstrapper/main.go b/cmd/security-bootstrapper/main.go new file mode 100644 index 0000000000..225ee5537f --- /dev/null +++ b/cmd/security-bootstrapper/main.go @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright 2021 Intel Corp. + * + * 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. + *******************************************************************************/ + +package main + +import ( + "context" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper" + "github.com/gorilla/mux" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + bootstrapper.Main(ctx, cancel, mux.NewRouter(), nil) +} diff --git a/cmd/security-bootstrapper/res/configuration.toml b/cmd/security-bootstrapper/res/configuration.toml new file mode 100644 index 0000000000..d99769cf40 --- /dev/null +++ b/cmd/security-bootstrapper/res/configuration.toml @@ -0,0 +1,27 @@ +[Writable] +LogLevel = 'INFO' + +[StageGate] + [StageGate.BootStrapper] + Host = "edgex-security-bootstrapper" + StartPort = 54321 + [StageGate.Ready] + ToRunPort = 54329 + [StageGate.SecretStoreSetup] + Host = "edgex-secretstore-setup" + [StageGate.SecretStoreSetup.Tokens] + ReadyPort = 54322 + [StageGate.Database] + # this is intended to be the same as Database.Primary.Host/.Port for other services + Host = "edgex-redis" + Port = 6379 + ReadyPort = 54323 + [StageGate.Registry] + # this is intended to be the same as Registry.Host/.Port for other services + Host = "edgex-core-consul" + Port = 8500 + ReadyPort = 54324 + [StageGate.KongDb] + Host = "kong-db" + Port = 5432 + ReadyPort = 54325 diff --git a/cmd/security-secretstore-setup/entrypoint.sh b/cmd/security-secretstore-setup/entrypoint.sh index 99c931e4f9..d3ecf2b3a1 100644 --- a/cmd/security-secretstore-setup/entrypoint.sh +++ b/cmd/security-secretstore-setup/entrypoint.sh @@ -14,37 +14,30 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# SPDX-License-Identifier: Apache-2.0' +# SPDX-License-Identifier: Apache-2.0 # ---------------------------------------------------------------------------------- set -e -if [ -n "${SECRETSTORE_SETUP_DONE_FLAG}" ] && [ -f "${SECRETSTORE_SETUP_DONE_FLAG}" ]; then - echo "Clearing secretstore-setup completion flag" - rm -f "${SECRETSTORE_SETUP_DONE_FLAG}" -fi - -echo "Starting vault-worker..." +# env settings are populated from env files of docker-compose echo "Initializing secret store..." /security-secretstore-setup --vaultInterval=10 -# write a sentinel file when we're done because consul is not -# secure and we don't trust it it access to the EdgeX secret store -if [ -n "${SECRETSTORE_SETUP_DONE_FLAG}" ]; then - # default User and Group in case never set - if [ -z "${EDGEX_USER}" ]; then - EDGEX_USER="2002" - EDGEX_GROUP="2001" - fi +# default User and Group in case never set +if [ -z "${EDGEX_USER}" ]; then + EDGEX_USER="2002" + EDGEX_GROUP="2001" +fi - echo "Changing ownership of secrets to ${EDGEX_USER}:${EDGEX_GROUP}" - chown -Rh ${EDGEX_USER}:${EDGEX_GROUP} /tmp/edgex/secrets +# /tmp/edgex/secrets need to be shared with all other services that need secrets and +# thus change the ownership to EDGEX_USER:EDGEX_GROUP +echo "$(date) Changing ownership of secrets to ${EDGEX_USER}:${EDGEX_GROUP}" +chown -Rh ${EDGEX_USER}:${EDGEX_GROUP} /tmp/edgex/secrets - echo "Signaling secretstore-setup completion" - mkdir -p $(dirname "${SECRETSTORE_SETUP_DONE_FLAG}") && \ - touch "${SECRETSTORE_SETUP_DONE_FLAG}" +# Signal tokens ready port for other services waiting on +/edgex-init/security-bootstrapper --confdir=/edgex-init/res listenTcp \ + --port="${STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT}" --host="${STAGEGATE_SECRETSTORESETUP_HOST}" +if [ $? -ne 0 ]; then + echo "$(date) failed to gating the tokens ready port" fi - -echo "Waiting for termination signal" -exec tail -f /dev/null diff --git a/cmd/support-notifications/Dockerfile b/cmd/support-notifications/Dockerfile index 209464d25a..a49560846e 100644 --- a/cmd/support-notifications/Dockerfile +++ b/cmd/support-notifications/Dockerfile @@ -34,18 +34,17 @@ RUN go mod download COPY . . RUN make cmd/support-notifications/support-notifications -FROM scratch +FROM alpine:3.12 + +RUN apk add --update --no-cache ca-certificates dumb-init LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2018: Cavium' + copyright='Copyright (c) 2018: Cavium, Copyright (c) 2021: Intel Corporation' ENV APP_PORT=48060 #expose support notifications port EXPOSE $APP_PORT -COPY --from=builder /bin/sh /bin/sh -COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates -COPY --from=builder /etc/ssl /etc/ssl COPY --from=builder /edgex-go/Attribution.txt / COPY --from=builder /edgex-go/cmd/support-notifications/support-notifications / COPY --from=builder /edgex-go/cmd/support-notifications/res/configuration.toml /res/configuration.toml diff --git a/cmd/support-scheduler/Dockerfile b/cmd/support-scheduler/Dockerfile index 4c334de2c5..37636a316e 100644 --- a/cmd/support-scheduler/Dockerfile +++ b/cmd/support-scheduler/Dockerfile @@ -35,10 +35,12 @@ RUN go mod download COPY . . RUN make cmd/support-scheduler/support-scheduler -FROM scratch +FROM alpine:3.12 + +RUN apk add --update --no-cache dumb-init LABEL license='SPDX-License-Identifier: Apache-2.0' \ - copyright='Copyright (c) 2018: Dell, Cavium' + copyright='Copyright (c) 2018: Dell, Cavium, Copyright (c) 2021: Intel Corporation' ENV APP_PORT=48085 #expose support scheduler port @@ -49,4 +51,4 @@ COPY --from=builder /edgex-go/cmd/support-scheduler/support-scheduler / COPY --from=builder /edgex-go/cmd/support-scheduler/res/configuration.toml /res/configuration.toml ENTRYPOINT ["/support-scheduler"] -CMD ["-cp=consul.http://edgex-core-consul:8500", "--registry", "--confdir=/res"] \ No newline at end of file +CMD ["-cp=consul.http://edgex-core-consul:8500", "--registry", "--confdir=/res"] diff --git a/go.mod b/go.mod index afadde626b..07624c6269 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/google/uuid v1.1.5 github.com/gorilla/mux v1.8.0 github.com/imdario/mergo v0.3.11 + github.com/lib/pq v1.9.0 github.com/pkg/errors v0.8.1 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/stretchr/testify v1.6.1 diff --git a/internal/security/bootstrapper/command/cmd_dispatcher.go b/internal/security/bootstrapper/command/cmd_dispatcher.go new file mode 100644 index 0000000000..a7e68e5ec8 --- /dev/null +++ b/internal/security/bootstrapper/command/cmd_dispatcher.go @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package command + +import ( + "context" + "fmt" + "sync" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/gate" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/genpassword" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/gethttpstatus" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/listen" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/ping" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +// NewCommand instantiates a command implementing interfaces.Command based on the input command argument +func NewCommand( + ctx context.Context, + wg *sync.WaitGroup, + lc logger.LoggingClient, + configuration *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + var command interfaces.Command + var err error + + if len(args) < 1 { + return nil, fmt.Errorf("subcommand required (%s, %s, %s, %s, %s)", gate.CommandName, listen.CommandName, + ping.CommandName, gethttpstatus.CommandName, genpassword.CommandName) + } + + commandName := args[0] + + switch commandName { + case gate.CommandName: + command, err = gate.NewCommand(ctx, wg, lc, configuration, args[1:]) + case listen.CommandName: + command, err = listen.NewCommand(ctx, wg, lc, configuration, args[1:]) + case ping.CommandName: + command, err = ping.NewCommand(ctx, wg, lc, configuration, args[1:]) + case gethttpstatus.CommandName: + command, err = gethttpstatus.NewCommand(ctx, wg, lc, configuration, args[1:]) + case genpassword.CommandName: + command, err = genpassword.NewCommand(ctx, wg, lc, configuration, args[1:]) + default: + command = nil + err = fmt.Errorf("unsupported command %s", commandName) + } + + return command, err +} diff --git a/internal/security/bootstrapper/command/cmd_dispatcher_test.go b/internal/security/bootstrapper/command/cmd_dispatcher_test.go new file mode 100644 index 0000000000..fcebe50592 --- /dev/null +++ b/internal/security/bootstrapper/command/cmd_dispatcher_test.go @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package command + +import ( + "context" + "sync" + "testing" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedCmdName string + expectedErr bool + }{ + {"Good: gate command", []string{"gate"}, "gate", false}, + {"Good: pingPgDb command only", []string{"pingPgDb"}, "pingPgDb", false}, + {"Good: pingPgDb command with options", []string{"pingPgDb", "--host=kong-db", "--port=5432"}, "pingPgDb", false}, + {"Good: listenTcp command", []string{"listenTcp", "--port=55555"}, "listenTcp", false}, + {"Good: genPassword command", []string{"genPassword"}, "genPassword", false}, + {"Good: getHttpStatus command", []string{"getHttpStatus", "--url=http://localhost:55555"}, "getHttpStatus", false}, + {"Bad: unknown command", []string{"unknown"}, "", true}, + {"Bad: empty command", []string{}, "", true}, + {"Bad: listenTcp command missing required --port", []string{"listenTcp"}, "", true}, + {"Bad: getHttpStatus command missing required --url", []string{"getHttpStatus"}, "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Act + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + + // Assert + if tt.expectedErr { + require.Error(t, err) + require.Nil(t, command) + } else { + require.NoError(t, err) + require.NotNil(t, command) + require.Equal(t, tt.expectedCmdName, command.GetCommandName()) + } + }) + } +} diff --git a/internal/security/bootstrapper/command/flags_common.go b/internal/security/bootstrapper/command/flags_common.go new file mode 100644 index 0000000000..b9999f73f8 --- /dev/null +++ b/internal/security/bootstrapper/command/flags_common.go @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package command + +import ( + "flag" + "fmt" + "os" + + "github.com/edgexfoundry/edgex-go/internal" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" +) + +// commonFlags is a custom implementation of flags.Common from go-mod-bootstrap +type commonFlags struct { + configDir string +} + +// NewCommonFlags creates new CommonFlags and initializes it +func NewCommonFlags() flags.Common { + commonFlags := commonFlags{} + return &commonFlags +} + +// Parse parses the command-line arguments +func (f *commonFlags) Parse(_ []string) { + flag.StringVar(&f.configDir, "confdir", "", "") + flag.Usage = HelpCallback + + flag.Parse() + + if flag.NArg() < 1 { + fmt.Println("Please specify command for " + os.Args[0]) + flag.Usage() + os.Exit(0) + } + + // Make sure Configuration Provider environment variable isn't set since this service doesn't support using it. + _ = os.Setenv(internal.ConfigProviderEnvVar, "") +} + +// ConfigFileName returns the name of the local configuration file +func (f *commonFlags) ConfigFileName() string { + return internal.ConfigFileName +} + +// OverwriteConfig returns false since the Configuration provider is not used +func (f *commonFlags) OverwriteConfig() bool { + return false +} + +// UseRegistry returns false since registry is not used +func (f *commonFlags) UseRegistry() bool { + return false +} + +// ConfigProviderUrl returns the empty url since Configuration Provider is not used. +func (f *commonFlags) ConfigProviderUrl() string { + return "" +} + +// Profile returns the empty name since profile is not used +func (f *commonFlags) Profile() string { + return "" +} + +// ConfigDirectory returns the directory where the config file(s) are located, if it was specified. +func (f *commonFlags) ConfigDirectory() string { + return f.configDir +} + +// Help displays the usage help message and exit. +func (f *commonFlags) Help() { + HelpCallback() +} + +// HelpCallback displays the help usage message and exits +func HelpCallback() { + fmt.Printf( + "Usage: %s [options] [arg...]\n"+ + "Options:\n"+ + " -h, --help Show this message\n"+ + " --confdir Specify local configuration directory\n"+ + "\n"+ + "Commands:\n"+ + " help Show available commands (this text)\n"+ + " gate Do security bootstrapper gating on stages while starting services\n"+ + " listenTcp Start up a TCP listener\n"+ + " pingPgDb Test Postgres database readiness\n"+ + " getHttpStatus Do an HTTP GET call to get the status code\n"+ + " genPassword Generate a random password\n", + os.Args[0]) +} diff --git a/internal/security/bootstrapper/command/gate/command.go b/internal/security/bootstrapper/command/gate/command.go new file mode 100644 index 0000000000..4879b58dc6 --- /dev/null +++ b/internal/security/bootstrapper/command/gate/command.go @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package gate + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "sync" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/tcp" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + // the command name for gating the stages of bootstrapping on other services for security + CommandName string = "gate" +) + +type cmd struct { + cntx context.Context + waitGroup *sync.WaitGroup + loggingClient logger.LoggingClient + config *config.ConfigurationStruct +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + ctx context.Context, + wg *sync.WaitGroup, + lc logger.LoggingClient, + conf *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + cmd := cmd{ + loggingClient: lc, + config: conf, + cntx: ctx, + waitGroup: wg, + } + var dummy string + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + flagSet.StringVar(&dummy, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + return &cmd, nil +} + +// Execute implements Command and runs this command +// command gate gates stages for bootstrapping EdgeX services for security +func (c *cmd) Execute() (statusCode int, err error) { + c.loggingClient.Infof("Security bootstrapper running %s", CommandName) + + bootstrapServer := tcp.NewTcpServer() + c.loggingClient.Debugf("init phase: attempts to start up the listener on bootstrap host: %s, port: %d", + c.config.StageGate.BootStrapper.Host, c.config.StageGate.BootStrapper.StartPort) + + // in a separate go-routine so it won't block the main thread execution + go openGatingSemaphorePort(bootstrapServer, c.config.StageGate.BootStrapper.StartPort, c.loggingClient, + "Raising bootstrap semaphore for secure bootstrapping") + + // wait on for others to be done: each of tcp dialers is a blocking call + c.loggingClient.Debug("Waiting on dependent semaphores required to raise the ready-to-run semaphore ...") + if err := tcp.DialTcp( + c.config.StageGate.Registry.Host, + c.config.StageGate.Registry.ReadyPort, + c.loggingClient); err != nil { + retErr := fmt.Errorf("found error while waiting for readiness of Registry at %s:%d, err: %v", + c.config.StageGate.Registry.Host, c.config.StageGate.Registry.ReadyPort, err) + return interfaces.StatusCodeExitWithError, retErr + } + c.loggingClient.Info("Registry is ready") + + if err := tcp.DialTcp( + c.config.StageGate.KongDB.Host, + c.config.StageGate.KongDB.ReadyPort, + c.loggingClient); err != nil { + retErr := fmt.Errorf("found error while waiting for readiness of KongDB at %s:%d, err: %v", + c.config.StageGate.KongDB.Host, c.config.StageGate.KongDB.ReadyPort, err) + return interfaces.StatusCodeExitWithError, retErr + } + c.loggingClient.Info("KongDB is ready") + + if err := tcp.DialTcp( + c.config.StageGate.Database.Host, + c.config.StageGate.Database.ReadyPort, + c.loggingClient); err != nil { + retErr := fmt.Errorf("found error while waiting for readiness of Database at %s:%d, err: %v", + c.config.StageGate.Database.Host, c.config.StageGate.Database.ReadyPort, err) + return interfaces.StatusCodeExitWithError, retErr + } + c.loggingClient.Info("Database is ready") + + // Reached ready-to-run phase + c.loggingClient.Debugf("ready-to-run phase: attempts to start up the listener on ready-to-run port: %d", + c.config.StageGate.Ready.ToRunPort) + + readyToRunServer := tcp.NewTcpServer() + + go openGatingSemaphorePort(readyToRunServer, c.config.StageGate.Ready.ToRunPort, c.loggingClient, + "Raising ready-to-run semaphore for secure bootstrapping") + + // keep running until ctx done + c.waitGroup.Add(1) + go func() { + defer c.waitGroup.Done() + + <-c.cntx.Done() + c.loggingClient.Info("security bootstrapper finished") + }() + + return +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} + +func openGatingSemaphorePort(tcpServer *tcp.TcpServer, portNum int, lc logger.LoggingClient, raisingMsg string) { + lc.Info(raisingMsg) + if err := tcpServer.StartListener(portNum, lc, ""); err != nil { + // listener is blocking forever until some internal critical error happens + lc.Error(err.Error()) + os.Exit(1) + } +} diff --git a/internal/security/bootstrapper/command/gate/command_test.go b/internal/security/bootstrapper/command/gate/command_test.go new file mode 100644 index 0000000000..36ac2d06ca --- /dev/null +++ b/internal/security/bootstrapper/command/gate/command_test.go @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package gate + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/tcp" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: gate cmd empty option", []string{}, false}, + {"Bad: gate invalid option", []string{"--invalid=xxx"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, command) + } + }) + } +} + +type testConfig struct { + testHost string + bootstrapperStartPort int + registryReadyPort int + databaseReadyPort int + kongDBReadyPort int + readyToRunPort int +} + +func TestExecuteWithAllDependentsRun(t *testing.T) { + // Arrange + lc := logger.MockLogger{} + testHost := "localhost" + testConfig := &testConfig{ + testHost: "localhost", + bootstrapperStartPort: 28001, + registryReadyPort: 28002, + databaseReadyPort: 28003, + kongDBReadyPort: 28004, + readyToRunPort: 28009, + } + config := setupMockServiceConfigs(testConfig) + + type executeReturn struct { + code int + err error + } + + ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) + wg := &sync.WaitGroup{} + gate, err := NewCommand(ctx, wg, lc, config, []string{}) + defer func() { + cancelFunc() + wg.Wait() + }() + require.NoError(t, err) + require.NotNil(t, gate) + require.Equal(t, "gate", gate.GetCommandName()) + execRet := make(chan executeReturn, 1) + // in a separate go-routine since the listenTcp is a blocking call + go func() { + statusCode, err := gate.Execute() + execRet <- executeReturn{code: statusCode, err: err} + }() + + tcpSrvErr := make(chan error) + // start up all other dependent mock services: + go func() { + tcpSrvErr <- tcp.NewTcpServer().StartListener(testConfig.registryReadyPort, + lc, testHost) + }() + go func() { + tcpSrvErr <- tcp.NewTcpServer().StartListener(testConfig.kongDBReadyPort, + lc, testHost) + }() + go func() { + tcpSrvErr <- tcp.NewTcpServer().StartListener(testConfig.databaseReadyPort, + lc, testHost) + }() + + select { + case ret := <-execRet: + require.NoError(t, ret.err) + require.Equal(t, interfaces.StatusCodeExitNormal, ret.code) + case err := <-tcpSrvErr: + require.NoError(t, err) + case <-time.After(5 * time.Second): + require.Fail(t, "security bootstrapper gate never returned") + } +} + +func setupMockServiceConfigs(testConf *testConfig) *config.ConfigurationStruct { + conf := &config.ConfigurationStruct{} + conf.StageGate = config.StageGateInfo{ + BootStrapper: config.BootStrapperInfo{ + Host: testConf.testHost, + StartPort: testConf.bootstrapperStartPort, + }, + Registry: config.RegistryInfo{ + Host: testConf.testHost, + Port: 12001, + ReadyPort: testConf.registryReadyPort, + }, + Database: config.DatabaseInfo{ + Host: testConf.testHost, + Port: 12002, + ReadyPort: testConf.databaseReadyPort, + }, + KongDB: config.KongDBInfo{ + Host: testConf.testHost, + Port: 12003, + ReadyPort: testConf.kongDBReadyPort, + }, + Ready: config.ReadyInfo{ + ToRunPort: testConf.readyToRunPort, + }, + } + return conf +} diff --git a/internal/security/bootstrapper/command/genpassword/command.go b/internal/security/bootstrapper/command/genpassword/command.go new file mode 100644 index 0000000000..eaad93f35a --- /dev/null +++ b/internal/security/bootstrapper/command/genpassword/command.go @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package genpassword + +import ( + "context" + "crypto/rand" + "encoding/base64" + "flag" + "fmt" + "os" + "strings" + "sync" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + CommandName string = "genPassword" + + randomBytesLength = 33 // 264 bits of entropy +) + +type cmd struct { + loggingClient logger.LoggingClient + config *config.ConfigurationStruct +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + _ context.Context, + _ *sync.WaitGroup, + lc logger.LoggingClient, + conf *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + cmd := cmd{ + loggingClient: lc, + config: conf, + } + var dummy string + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + flagSet.StringVar(&dummy, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + return &cmd, nil +} + +// Execute implements Command and runs this command +// command genPassword generates a random password +func (c *cmd) Execute() (int, error) { + c.loggingClient.Infof("Security bootstrapper running %s", CommandName) + + randomBytes := make([]byte, randomBytesLength) + _, err := rand.Read(randomBytes) // all of salt guaranteed to be filled if err==nil + if err != nil { + return interfaces.StatusCodeExitWithError, err + } + + randPass := base64.StdEncoding.EncodeToString(randomBytes) + // output the randPass to stdout + fmt.Fprintln(os.Stdout, randPass) + + return interfaces.StatusCodeExitNormal, nil +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} diff --git a/internal/security/bootstrapper/command/genpassword/command_test.go b/internal/security/bootstrapper/command/genpassword/command_test.go new file mode 100644 index 0000000000..f37edf6ba4 --- /dev/null +++ b/internal/security/bootstrapper/command/genpassword/command_test.go @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package genpassword + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: genPasswd cmd empty option", []string{}, false}, + {"Bad: genPasswd invalid option", []string{"--invalid=xxx"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, command) + } + }) + } +} + +func TestExecute(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + genPwd, err := NewCommand(ctx, wg, lc, config, []string{}) + require.NoError(t, err) + require.NotNil(t, genPwd) + require.Equal(t, "genPassword", genPwd.GetCommandName()) + + statusCode, err := genPwd.Execute() + require.NoError(t, err) + require.Equal(t, interfaces.StatusCodeExitNormal, statusCode) +} diff --git a/internal/security/bootstrapper/command/gethttpstatus/command.go b/internal/security/bootstrapper/command/gethttpstatus/command.go new file mode 100644 index 0000000000..b72a4aa2c3 --- /dev/null +++ b/internal/security/bootstrapper/command/gethttpstatus/command.go @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package gethttpstatus + +import ( + "context" + "flag" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "sync" + + "github.com/edgexfoundry/edgex-go/internal" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/secretstoreclient" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + CommandName string = "getHttpStatus" +) + +type cmd struct { + loggingClient logger.LoggingClient + client internal.HttpCaller + configuration *config.ConfigurationStruct + + // options + httpURL string +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + _ context.Context, + _ *sync.WaitGroup, + lc logger.LoggingClient, + configuration *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + cmd := cmd{ + loggingClient: lc, + client: secretstoreclient.NewRequestor(lc).Insecure(), + configuration: configuration, + } + var dummy string + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + flagSet.StringVar(&dummy, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + flagSet.StringVar(&cmd.httpURL, "url", "", "get the status code returning from the input http URL address") + + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + if len(cmd.httpURL) == 0 { + return nil, fmt.Errorf("%s %s: argument --url is required", os.Args[0], CommandName) + } + + return &cmd, nil +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} + +// Execute implements Command and runs this command +// command getHttpStatus makes a http GET request and outputs the http status code +func (c *cmd) Execute() (int, error) { + c.loggingClient.Infof("Security bootstrapper running %s", CommandName) + + _, err := url.Parse(c.httpURL) + if err != nil { + return interfaces.StatusCodeExitWithError, err + } + + c.loggingClient.Infof("http calls on the endpoint of %s", c.httpURL) + req, err := http.NewRequest(http.MethodGet, c.httpURL, http.NoBody) + if err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to prepare request for http URL: %w", err) + } + resp, err := c.client.Do(req) + if err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to send request for http URL: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + // don't care about the response body, only the status code + fmt.Fprintln(os.Stdout, resp.StatusCode) + + return interfaces.StatusCodeExitNormal, nil +} diff --git a/internal/security/bootstrapper/command/gethttpstatus/command_test.go b/internal/security/bootstrapper/command/gethttpstatus/command_test.go new file mode 100644 index 0000000000..fa4f815aa5 --- /dev/null +++ b/internal/security/bootstrapper/command/gethttpstatus/command_test.go @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package gethttpstatus + +import ( + "context" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: getHttpStatus required --url option", []string{"--url=http://localhost:32323"}, false}, + {"Bad: getHttpStatus invalid option", []string{"--invalid=http://localhost:123"}, true}, + {"Bad: getHttpStatus empty option", []string{""}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, command) + } + }) + } +} + +func TestExecute(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + testSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer testSrv.Close() + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: getHttpStatus with existing server", []string{"--url=" + testSrv.URL}, false}, + {"Bad: getHttpStatus with non-existing server", []string{"--url=http://non-existing:1111"}, true}, + {"Bad: getHttpStatus with malformed URL", []string{"--url=_http!@xxxxxx:1111"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getHttpStatus, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + require.NoError(t, err) + require.NotNil(t, getHttpStatus) + require.Equal(t, "getHttpStatus", getHttpStatus.GetCommandName()) + + statusCode, err := getHttpStatus.Execute() + + if tt.expectedErr { + require.Error(t, err) + require.Equal(t, interfaces.StatusCodeExitWithError, statusCode) + } else { + require.NoError(t, err) + require.Equal(t, interfaces.StatusCodeExitNormal, statusCode) + } + }) + } +} diff --git a/internal/security/bootstrapper/command/help/command.go b/internal/security/bootstrapper/command/help/command.go new file mode 100644 index 0000000000..b541ff7bf1 --- /dev/null +++ b/internal/security/bootstrapper/command/help/command.go @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package help + +import ( + "flag" + "fmt" + "strings" + + bootstrapper "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + CommandName = "help" +) + +type cmd struct { + loggingClient logger.LoggingClient + configuration *config.ConfigurationStruct + flagSet *flag.FlagSet +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + lc logger.LoggingClient, + configuration *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + return &cmd{ + loggingClient: lc, + configuration: configuration, + flagSet: flagSet, + }, nil +} + +// Execute implements Command and runs this command +// command help prints the usage for security-bootstrapper +func (c *cmd) Execute() (statusCode int, err error) { + bootstrapper.HelpCallback() + return interfaces.StatusCodeExitNormal, nil +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} diff --git a/internal/security/bootstrapper/command/help/command_test.go b/internal/security/bootstrapper/command/help/command_test.go new file mode 100644 index 0000000000..404dbf6e4a --- /dev/null +++ b/internal/security/bootstrapper/command/help/command_test.go @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package help + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +// TestHelp tests functionality of help command +func TestHelp(t *testing.T) { + // Arrange + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + // Act + command, err := NewCommand(lc, config, []string{}) + require.NoError(t, err) + + code, err := command.Execute() + + // Assert + require.NoError(t, err) + require.Equal(t, interfaces.StatusCodeExitNormal, code) +} + +// TestHelpBadArg tests unknown arg handler +func TestHelpBadArg(t *testing.T) { + // Arrange + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + // Act + command, err := NewCommand(lc, config, []string{"-badarg"}) + + // Assert + require.Error(t, err) + require.Nil(t, command) +} diff --git a/internal/security/bootstrapper/command/listen/command.go b/internal/security/bootstrapper/command/listen/command.go new file mode 100644 index 0000000000..85364e30d0 --- /dev/null +++ b/internal/security/bootstrapper/command/listen/command.go @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package listen + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "sync" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/tcp" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + CommandName string = "listenTcp" +) + +type cmd struct { + loggingClient logger.LoggingClient + config *config.ConfigurationStruct + + // options: + tcpHost string + tcpPort int +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + _ context.Context, + _ *sync.WaitGroup, + lc logger.LoggingClient, + conf *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + cmd := cmd{ + loggingClient: lc, + config: conf, + } + var dummy string + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + flagSet.StringVar(&dummy, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + flagSet.StringVar(&cmd.tcpHost, "host", "", "the hostname of TCP server to listen ") + + flagSet.IntVar(&cmd.tcpPort, "port", 0, "the port number of TCP server to listen ") + + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + if cmd.tcpPort == 0 { + return nil, fmt.Errorf("%s %s: argument --port is required", os.Args[0], CommandName) + } + + return &cmd, nil +} + +// Execute implements Command and runs this command +// command listenTcp starts a TCP listener with configured port and host +func (c *cmd) Execute() (int, error) { + c.loggingClient.Infof("Security bootstrapper running %s", CommandName) + + tcpServer := tcp.NewTcpServer() + + // block and listening forever until internal error + if err := tcpServer.StartListener(c.tcpPort, c.loggingClient, c.tcpHost); err != nil { + return interfaces.StatusCodeExitWithError, err + } + + return interfaces.StatusCodeExitNormal, nil +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} diff --git a/internal/security/bootstrapper/command/listen/command_test.go b/internal/security/bootstrapper/command/listen/command_test.go new file mode 100644 index 0000000000..ab5ebf76bb --- /dev/null +++ b/internal/security/bootstrapper/command/listen/command_test.go @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package listen + +import ( + "context" + "strconv" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/tcp" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: listenTcp required --port option", []string{"--port=32323"}, false}, + {"Good: listenTcp both options", []string{"--host=test", "--port=32323"}, false}, + {"Bad: listenTcp invalid option", []string{"--invalid=xxxxx"}, true}, + {"Bad: listenTcp empty option", []string{""}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, command) + } + }) + } +} + +func TestExecute(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + testListenPort1 := 65111 + testListenPort2 := 65112 + + tests := []struct { + name string + cmdArgs []string + testPort int + expectedErr bool + }{ + {"Good: listenTcp with unbound port", + []string{"--port=" + strconv.Itoa(testListenPort1)}, testListenPort1, false}, + {"Good: listenTcp with unbound port specific host", + []string{"--host=localhost", "--port=" + strconv.Itoa(testListenPort2)}, testListenPort2, false}, + {"Bad: listenTcp with already bound port", + []string{"--host=localhost", "--port=" + strconv.Itoa(testListenPort2)}, testListenPort2, true}, + } + + type executeReturn struct { + code int + err error + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + listenTcp, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + require.NoError(t, err) + require.NotNil(t, listenTcp) + require.Equal(t, "listenTcp", listenTcp.GetCommandName()) + + execRet := make(chan executeReturn, 1) + // in a separate go-routine since the listenTcp is a blocking call + go func() { + statusCode, err := listenTcp.Execute() + execRet <- executeReturn{code: statusCode, err: err} + }() + + dialErr := make(chan error) + // dial to tcp server to check the server being listening + go func() { + dialErr <- tcp.DialTcp("", tt.testPort, lc) + }() + + select { + case testErr := <-dialErr: + require.NoError(t, testErr) + case <-time.After(3 * time.Second): + require.Fail(t, "DialTcp never returned") + } + + // test to wait for some time to check the running tcp server is not errorred out + // since receiving execRet channel will block forever if no error occurs + select { + case ret := <-execRet: + if tt.expectedErr { + require.Error(t, ret.err) + require.Equal(t, interfaces.StatusCodeExitWithError, ret.code) + } else { + require.NoError(t, ret.err) + require.Equal(t, interfaces.StatusCodeExitNormal, ret.code) + } + case <-time.After(5 * time.Second): + t.Logf("tcp server %s listening ok", tt.cmdArgs) + } + }) + } +} diff --git a/internal/security/bootstrapper/command/ping/command.go b/internal/security/bootstrapper/command/ping/command.go new file mode 100644 index 0000000000..3daa5fa9ec --- /dev/null +++ b/internal/security/bootstrapper/command/ping/command.go @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package ping + +import ( + "context" + "database/sql" + "flag" + "fmt" + "os" + "strconv" + "strings" + "sync" + + _ "github.com/lib/pq" + + "github.com/edgexfoundry/edgex-go/internal" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + "github.com/edgexfoundry/edgex-go/internal/security/secretstoreclient" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + CommandName string = "pingPgDb" +) + +type cmd struct { + loggingClient logger.LoggingClient + client internal.HttpCaller + configuration *config.ConfigurationStruct + + // options + host string + port int + username string + dbname string + pwd string +} + +// NewCommand creates a new cmd and parses through options if any +func NewCommand( + _ context.Context, + _ *sync.WaitGroup, + lc logger.LoggingClient, + configuration *config.ConfigurationStruct, + args []string) (interfaces.Command, error) { + + cmd := cmd{ + loggingClient: lc, + client: secretstoreclient.NewRequestor(lc).Insecure(), + configuration: configuration, + } + + var dummy string + + flagSet := flag.NewFlagSet(CommandName, flag.ContinueOnError) + flagSet.StringVar(&dummy, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + + flagSet.StringVar(&cmd.host, "host", cmd.configuration.StageGate.KongDB.Host, "the hostname of postgres database; "+ + cmd.configuration.StageGate.KongDB.Host+" will be use if omitted") + + flagSet.IntVar(&cmd.port, "port", cmd.configuration.StageGate.KongDB.Port, "the port number of postgres database; "+ + strconv.Itoa(configuration.StageGate.KongDB.Port)+" will be use if omitted") + + flagSet.StringVar(&cmd.username, "username", "postgres", "the username of postgres database; "+ + "postgres will be use if omitted") + + flagSet.StringVar(&cmd.dbname, "dbname", "", "the database instance name of postgres database; "+ + "this is required for pinging the readiness of the database") + + flagSet.StringVar(&cmd.pwd, "password", "", "the user's password of postgres database; "+ + "this is required for pinging the readiness of the database") + + err := flagSet.Parse(args) + if err != nil { + return nil, fmt.Errorf("Unable to parse command: %s: %w", strings.Join(args, " "), err) + } + + return &cmd, nil +} + +// Execute implements Command and runs this command +// command pingPgDb pings the Postgres database with configured db info +func (c *cmd) Execute() (statusCode int, err error) { + c.loggingClient.Infof("Security bootstrapper running %s", CommandName) + + // test readiness of kong db + psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + c.host, c.port, c.username, c.pwd, c.dbname) + + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + return interfaces.StatusCodeExitWithError, fmt.Errorf("Failed to open sql driver connector: %v", err) + } + + defer func() { + _ = db.Close() + }() + + c.loggingClient.Debug("postgres sql driver connector opened") + + err = db.Ping() + if err != nil { + err = fmt.Errorf("failed to ping postgres database with provided db info: %v", err) + return interfaces.StatusCodeExitWithError, err + } + + c.loggingClient.Info("Postgres db is ready") + + // send to stdout so that the pinger can pick up the "ready" message + fmt.Fprintln(os.Stdout, "ready") + + return interfaces.StatusCodeExitNormal, nil +} + +// GetCommandName returns the name of this command +func (c *cmd) GetCommandName() string { + return CommandName +} diff --git a/internal/security/bootstrapper/command/ping/command_test.go b/internal/security/bootstrapper/command/ping/command_test.go new file mode 100644 index 0000000000..322265961c --- /dev/null +++ b/internal/security/bootstrapper/command/ping/command_test.go @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package ping + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestNewCommand(t *testing.T) { + // Arrange + ctx := context.Background() + wg := &sync.WaitGroup{} + lc := logger.MockLogger{} + config := &config.ConfigurationStruct{} + + tests := []struct { + name string + cmdArgs []string + expectedErr bool + }{ + {"Good: ping cmd empty option", []string{}, false}, + {"Bad: ping invalid option", []string{"--invalid=xxx"}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command, err := NewCommand(ctx, wg, lc, config, tt.cmdArgs) + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, command) + } + }) + } +} diff --git a/internal/security/bootstrapper/config/config.go b/internal/security/bootstrapper/config/config.go new file mode 100644 index 0000000000..9001def76c --- /dev/null +++ b/internal/security/bootstrapper/config/config.go @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package config + +import ( + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" +) + +type ConfigurationStruct struct { + Writable WritableInfo + StageGate StageGateInfo +} + +type WritableInfo struct { + LogLevel string +} + +// UpdateFromRaw converts configuration received from the registry to a service-specific configuration struct which is +// then used to overwrite the service's existing configuration struct. +func (c *ConfigurationStruct) UpdateFromRaw(rawConfig interface{}) bool { + configuration, ok := rawConfig.(*ConfigurationStruct) + if ok { + *c = *configuration + } + return ok +} + +// EmptyWritablePtr returns a pointer to a service-specific empty WritableInfo struct. It is used by the bootstrap to +// provide the appropriate structure to registry.Client's WatchForChanges(). +func (c *ConfigurationStruct) EmptyWritablePtr() interface{} { + return &WritableInfo{} +} + +// UpdateWritableFromRaw converts configuration received from the registry to a service-specific WritableInfo struct +// which is then used to overwrite the service's existing configuration's WritableInfo struct. +func (c *ConfigurationStruct) UpdateWritableFromRaw(rawWritable interface{}) bool { + writable, ok := rawWritable.(*WritableInfo) + if ok { + c.Writable = *writable + } + return ok +} + +// GetBootstrap returns the configuration elements required by the bootstrap. Currently, a copy of the configuration +// data is returned. This is intended to be temporary -- since ConfigurationStruct drives the configuration.toml's +// structure -- until we can make backwards-breaking configuration.toml changes (which would consolidate these fields +// into an bootstrapConfig.BootstrapConfiguration struct contained within ConfigurationStruct). +func (c *ConfigurationStruct) GetBootstrap() bootstrapConfig.BootstrapConfiguration { + // temporary until we can make backwards-breaking configuration.toml change + return bootstrapConfig.BootstrapConfiguration{} +} + +// GetLogLevel returns the current ConfigurationStruct's log level. +func (c *ConfigurationStruct) GetLogLevel() string { + return c.Writable.LogLevel +} + +// GetRegistryInfo returns the RegistryInfo from the ConfigurationStruct. +func (c *ConfigurationStruct) GetRegistryInfo() bootstrapConfig.RegistryInfo { + return bootstrapConfig.RegistryInfo{} +} + +// GetDatabaseInfo returns a database information map. +func (c *ConfigurationStruct) GetDatabaseInfo() map[string]bootstrapConfig.Database { + return nil +} + +// GetInsecureSecrets returns the service's InsecureSecrets. +func (c *ConfigurationStruct) GetInsecureSecrets() bootstrapConfig.InsecureSecrets { + return nil +} diff --git a/internal/security/bootstrapper/config/types.go b/internal/security/bootstrapper/config/types.go new file mode 100644 index 0000000000..18d285f522 --- /dev/null +++ b/internal/security/bootstrapper/config/types.go @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package config + +// BootStrapperInfo defines the first stage gate info +// It is the first stage gate of security bootstrapping +type BootStrapperInfo struct { + Host string + StartPort int +} + +// ReadyInfo defines the ready stage gate info +// It is the last stage gate of security bootstrapping for +// Kong, and all other Edgex core services +type ReadyInfo struct { + ToRunPort int +} + +// TokensInfo defines the tokens ready stage gate info +// for the secretstore setup (formerly known as vault-worker) +type TokensInfo struct { + ReadyPort int +} + +// SecretStoreSetupInfo defines the fields related to +// stage gating the secretstore setup (formerly known as vault-worker) bootstrapping +type SecretStoreSetupInfo struct { + Host string + Tokens TokensInfo +} + +// DatabaseInfo defines the fields related to +// stage gating the database bootstrapping +type DatabaseInfo struct { + Host string + Port int + ReadyPort int +} + +// RegistryInfo defines the fields related to +// stage gating the registry bootstrapping +type RegistryInfo struct { + Host string + Port int + ReadyPort int +} + +// KongDBInfo defines the fields related to +// stage gating the Kong's database bootstrapping +type KongDBInfo struct { + Host string + Port int + ReadyPort int +} + +// StageGateInfo defines the gate info for the security bootstrapper +// in different stages for services. From the TOML structure perspective, +// it is segmented in the way that environment variables are easier +// to read when they become all upper cases in the environment override. +type StageGateInfo struct { + BootStrapper BootStrapperInfo + Ready ReadyInfo + SecretStoreSetup SecretStoreSetupInfo + Database DatabaseInfo + Registry RegistryInfo + KongDB KongDBInfo +} diff --git a/internal/security/bootstrapper/container/container.go b/internal/security/bootstrapper/container/container.go new file mode 100644 index 0000000000..7a8c366c80 --- /dev/null +++ b/internal/security/bootstrapper/container/container.go @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package container + +import ( + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" +) + +// ConfigurationName contains the name of the config.ConfigurationStruct implementation in the DIC. +var ConfigurationName = di.TypeInstanceToName(config.ConfigurationStruct{}) + +// ConfigurationFrom helper function queries the DIC and returns the config.ConfigurationStruct implementation. +func ConfigurationFrom(get di.Get) *config.ConfigurationStruct { + return get(ConfigurationName).(*config.ConfigurationStruct) +} diff --git a/internal/security/bootstrapper/handlers/init.go b/internal/security/bootstrapper/handlers/init.go new file mode 100644 index 0000000000..3f03e435b6 --- /dev/null +++ b/internal/security/bootstrapper/handlers/init.go @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package handlers + +import ( + "context" + "flag" + "os" + "sync" + + bootstrapper "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command/help" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/container" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/interfaces" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" +) + +// Bootstrap is to implement BootstrapHandler +type Bootstrap struct { + exitStatusCode int +} + +// NewInitialization is to instantiate a Bootstrap instance +func NewInitialization() *Bootstrap { + return &Bootstrap{} +} + +// BootstrapHandler fulfills the BootstrapHandler contract and performs initialization needed +// for security bootstrapper's command arguments +func (b *Bootstrap) BootstrapHandler(ctx context.Context, wg *sync.WaitGroup, _ startup.Timer, dic *di.Container) bool { + conf := container.ConfigurationFrom(dic.Get) + lc := bootstrapContainer.LoggingClientFrom(dic.Get) + + lc.Debugf("configuration from the local TOML: %v", *conf) + + var command interfaces.Command + var err error + + var confdir string + flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flagSet.StringVar(&confdir, "confdir", "", "") // handled by bootstrap; duplicated here to prevent arg parsing errors + err = flagSet.Parse(os.Args[1:]) + if err != nil { + lc.Error(err.Error()) + } + + subcommandArgs := []string{} + + commandName := flagSet.Arg(0) + if flag.NArg() > 0 { + subcommandArgs = append(subcommandArgs, flag.Args()...) + } + + switch commandName { + case help.CommandName: + command, err = help.NewCommand(lc, conf, subcommandArgs) + default: + command, err = bootstrapper.NewCommand(ctx, wg, lc, conf, subcommandArgs) + if command == nil { + lc.Error(err.Error()) + b.exitStatusCode = interfaces.StatusCodeNoOptionSelected + return false + } + } + + if err != nil { + lc.Error(err.Error()) + b.exitStatusCode = interfaces.StatusCodeExitWithError + return false + } + + exitStatusCode, err := command.Execute() + if err != nil { + lc.Error(err.Error()) + } + b.exitStatusCode = exitStatusCode + + wg.Done() + + return true +} + +// GetExitStatusCode returns security bootstrapper's exit code +func (b *Bootstrap) GetExitStatusCode() int { + return b.exitStatusCode +} diff --git a/internal/security/bootstrapper/interfaces/command.go b/internal/security/bootstrapper/interfaces/command.go new file mode 100644 index 0000000000..ee81dddf49 --- /dev/null +++ b/internal/security/bootstrapper/interfaces/command.go @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package interfaces + +const ( + // StatusCodeExitNormal exit code for normal case + StatusCodeExitNormal = 0 + // StatusCodeNoOptionSelected exit code for missing options case + StatusCodeNoOptionSelected = 1 + // StatusCodeExitWithError is exit code for error case + StatusCodeExitWithError = 2 +) + +// Command implement the Command pattern +type Command interface { + Execute() (statusCode int, err error) + GetCommandName() string +} diff --git a/internal/security/bootstrapper/main.go b/internal/security/bootstrapper/main.go new file mode 100644 index 0000000000..3acdae818f --- /dev/null +++ b/internal/security/bootstrapper/main.go @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package bootstrapper + +import ( + "context" + "os" + + "github.com/gorilla/mux" + + "github.com/edgexfoundry/edgex-go/internal" + bootstrapper "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/command" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/config" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/container" + "github.com/edgexfoundry/edgex-go/internal/security/bootstrapper/handlers" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" +) + +const ( + securityBootstrapperServiceKey = "edgex-security-bootstrapper" +) + +// Main function is the wrapper for the security bootstrapper main +func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan<- bool) { + // service key for this bootstrapper service + startupTimer := startup.NewStartUpTimer(securityBootstrapperServiceKey) + + // Common Command-line flags have been moved to command.CommonFlags, but this service doesn't use all + // the common flags so we are using our own implementation of the CommonFlags interface + f := bootstrapper.NewCommonFlags() + + f.Parse(os.Args[1:]) + + configuration := &config.ConfigurationStruct{} + dic := di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return configuration + }, + }) + + serviceHandler := handlers.NewInitialization() + + bootstrap.Run( + ctx, + cancel, + f, + securityBootstrapperServiceKey, + internal.ConfigStemSecurity+internal.ConfigMajorVersion, + configuration, + startupTimer, + dic, + []interfaces.BootstrapHandler{ + serviceHandler.BootstrapHandler, + }, + ) + + // exit with the code specified by serviceHandler + os.Exit(serviceHandler.GetExitStatusCode()) +} diff --git a/internal/security/bootstrapper/tcp/client.go b/internal/security/bootstrapper/tcp/client.go new file mode 100644 index 0000000000..9cc4550012 --- /dev/null +++ b/internal/security/bootstrapper/tcp/client.go @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package tcp + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + dialTimeoutDuration = 5 * time.Second +) + +// DialTcp will instantiate a new TCP dialer trying to connect to the TCP server specified by host and port +// host name can be empty as indicated in Golang's godoc: https://godoc.org/net#Dial +// port number must be greater than 0 +func DialTcp(host string, port int, lc logger.LoggingClient) error { + tcpHost := strings.TrimSpace(host) + if port <= 0 { + return fmt.Errorf("for tcp dial, port number must be greater than 0: %d", port) + } + + tcpServerAddr := net.JoinHostPort(tcpHost, strconv.Itoa(port)) + + for { // keep trying until server connects + lc.Debugf("Trying to connecting to TCP server at address: %s", tcpServerAddr) + c, err := net.DialTimeout("tcp", tcpServerAddr, dialTimeoutDuration) + if err != nil { + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Op == "dial" { + lc.Infof("TCP server %s may be not ready yet, retry in 1 second", tcpServerAddr) + time.Sleep(time.Second) + continue + } else { + return err + } + } + defer func() { + _ = c.Close() + }() + + lc.Infof("Connected with TCP server %s", tcpServerAddr) + + // don't need to do anything else once it's connected in terms of response to the gating listener + break + } + return nil +} diff --git a/internal/security/bootstrapper/tcp/client_test.go b/internal/security/bootstrapper/tcp/client_test.go new file mode 100644 index 0000000000..abb978b660 --- /dev/null +++ b/internal/security/bootstrapper/tcp/client_test.go @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package tcp + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestDialTcpClient(t *testing.T) { + lc := logger.MockLogger{} + errs := make(chan error, 1) + testListeningPort := 12333 + srv := NewTcpServer() + go func() { + errs <- srv.StartListener(testListeningPort, lc, "") + }() + + time.Sleep(time.Second) + + tests := []struct { + name string + host string + port int + expectError bool + }{ + {"Good: Empty host input", "", testListeningPort, false}, + {"Bad: Port number = 0", "localhost", 0, true}, + {"Bad: Port number < 0", "localhost", -1, true}, + {"Bad: Both empty host and 0 port number", "", 0, true}, + {"Good: Dial the TCP server and port", "localhost", testListeningPort, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // dialTcp in goroutine so it won't block forever + go func() { + errs <- DialTcp(tt.host, tt.port, lc) + }() + + select { + case err := <-errs: + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + case <-time.After(2 * time.Second): + require.Fail(t, "DialTcp never returned") + } + }) + } +} + +func TestDialTcpNoTCPServer(t *testing.T) { + lc := logger.MockLogger{} + errs := make(chan error, 1) + testPort := 12349 + go func() { + errs <- DialTcp("127.0.0.1", testPort, lc) + }() + + select { + case err := <-errs: + require.NoError(t, err) + + // since the tcp server is never up, the dial will block forever, + // so we set a timeout for the test to signal it is done + case <-time.After(dialTimeoutDuration + time.Second): + fmt.Println("Expected timed out due to no TCP server") + } +} diff --git a/internal/security/bootstrapper/tcp/listener.go b/internal/security/bootstrapper/tcp/listener.go new file mode 100644 index 0000000000..9a53b24516 --- /dev/null +++ b/internal/security/bootstrapper/tcp/listener.go @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package tcp + +import ( + "bufio" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +const ( + connectionTimeout = 5 * time.Second +) + +type TcpServer struct { +} + +func NewTcpServer() *TcpServer { + return &TcpServer{} +} + +// StartListener instantiates a new listener on port and optional host if it is not empty +// returns error if failed to create a listener on the port number +func (tcpSrv *TcpServer) StartListener(port int, lc logger.LoggingClient, host string) error { + lc.Debugf("Starting listener on port %d ...", port) + + trimmedHost := strings.TrimSpace(host) + doneSrv := net.JoinHostPort(trimmedHost, strconv.Itoa(port)) + + listener, err := net.Listen("tcp", doneSrv) + if err != nil { + // nolint: staticcheck + return fmt.Errorf("Failed to create TCP listener: %v", err) + } + + defer func() { + _ = listener.Close() + }() + + lc.Infof("Security bootstrapper starts listening on tcp://%s", doneSrv) + for { + conn, err := listener.Accept() + if err != nil { + lc.Errorf("found error when accepting connection: %v ! retry again in one second", err) + time.Sleep(time.Second) + continue + } + + lc.Infof("Accepted connection on %s", doneSrv) + + // once reached here, the connection is established, and consider the semaphore on this port is raised + go func(c *net.Conn) { + defer func() { + _ = (*c).Close() + }() + + if err := handleConnection(*c); err != nil { + lc.Warnf("failed to write through connection on %s: %v", doneSrv, err) + } + + // intended process listener is done + lc.Debugf("connection on port %d is done", port) + }(&conn) + } +} + +func handleConnection(conn net.Conn) error { + bufWriter := bufio.NewWriter(conn) + datetime := time.Now().String() + _ = conn.SetWriteDeadline(time.Now().Add(connectionTimeout)) + _, err := bufWriter.Write([]byte(datetime)) + + return err +} diff --git a/internal/security/bootstrapper/tcp/listener_test.go b/internal/security/bootstrapper/tcp/listener_test.go new file mode 100644 index 0000000000..b7ff9ba168 --- /dev/null +++ b/internal/security/bootstrapper/tcp/listener_test.go @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright 2021 Intel Corporation + * + * 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. + * + *******************************************************************************/ + +package tcp + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" +) + +func TestStartListener(t *testing.T) { + lc := logger.MockLogger{} + errs := make(chan error, 1) + testPort := 12345 + srv := NewTcpServer() + // in a separate goroutine since listener is blocking the main test thread + go func() { + errs <- srv.StartListener(testPort, lc, "") + }() + + // in this test case we want to give some time for listener comes first + time.Sleep(2 * time.Second) + + go func() { + errs <- DialTcp("127.0.0.1", testPort, lc) + }() + + select { + case err := <-errs: + require.NoError(t, err) + case <-time.After(5 * time.Second): + require.Fail(t, "DialTcp never returned") + } +} + +func TestStartListenerAlreadyInUse(t *testing.T) { + lc := logger.MockLogger{} + errs := make(chan error, 1) + testPort := 12347 + srv1 := NewTcpServer() + go func() { + errs <- srv1.StartListener(testPort, lc, "") + }() + + // try to start another listener with the same port + // this will cause an error + srv2 := NewTcpServer() + go func() { + errs <- srv2.StartListener(testPort, lc, "") + }() + + select { + case err := <-errs: + require.Error(t, err) + case <-time.After(5 * time.Second): + require.Fail(t, "none of Tcp listeners never started") + } +} + +func TestStartListenerWithDialFirst(t *testing.T) { + lc := logger.MockLogger{} + errs := make(chan error, 1) + testPort := 12341 + srv := NewTcpServer() + + go func() { + errs <- DialTcp("127.0.0.1", testPort, lc) + }() + + // literally delay some time so that DialTcp always comes first + time.Sleep(2 * time.Second) + + // in a separate goroutine since listener is blocking the main test thread + go func() { + errs <- srv.StartListener(testPort, lc, "") + }() + + select { + case err := <-errs: + require.NoError(t, err) + case <-time.After(5 * time.Second): + require.Fail(t, "DialTcp never returned") + } +} diff --git a/internal/security/redis/main.go b/internal/security/redis/main.go index 183259d525..612d2fa038 100644 --- a/internal/security/redis/main.go +++ b/internal/security/redis/main.go @@ -33,7 +33,7 @@ import ( "github.com/gorilla/mux" ) -func Main(ctx context.Context, cancel context.CancelFunc, router *mux.Router, readyStream chan<- bool) { +func Main(ctx context.Context, cancel context.CancelFunc, _ *mux.Router, _ chan<- bool) { startupTimer := startup.NewStartUpTimer(clients.SecurityBootstrapRedisKey) // All common command-line flags have been moved to DefaultCommonFlags. @@ -52,7 +52,7 @@ func Main(ctx context.Context, cancel context.CancelFunc, router *mux.Router, re // bootstrap.RunAndReturnWaitGroup is needed for the underlying configuration system. // Conveniently, it also creates a pipeline of functions as the list of BootstrapHandler's is // executed in order. - bootstrap.RunAndReturnWaitGroup( + _, _, ok := bootstrap.RunAndReturnWaitGroup( ctx, cancel, f, @@ -69,4 +69,9 @@ func Main(ctx context.Context, cancel context.CancelFunc, router *mux.Router, re handler.maybeSetCredentials, }, ) + + if !ok { + // had some issue(s) during bootstrapping redis + os.Exit(1) + } }