├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile.openshift ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.adoc ├── config.yaml ├── configuration └── configuration.go ├── connection └── connection.go ├── main.go ├── server ├── server.go └── server_test.go ├── storage ├── repository.go └── url.go └── templates ├── database-deployment.yml ├── database-secrets.yml ├── database-service.yml ├── database-storage.yml ├── namespace.yml ├── webapp-config.yaml ├── webapp-deploymentconfig.yml ├── webapp-imagestream.yml └── webapp-service.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | vendor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | tmp 3 | .tmp 4 | go-url-shortener 5 | bin -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build image 2 | FROM centos:7 as builder 3 | LABEL author="Xavier Coulon " 4 | ENV LANG=en_US.utf8 5 | 6 | # Install wget and git 7 | RUN yum --enablerepo=centosplus install -y \ 8 | wget \ 9 | git 10 | 11 | # install golang 1.9 12 | ENV GOLANG_VERSION=1.9.2 13 | RUN wget -O /opt/go${GOLANG_VERSION}.linux-amd64.tar.gz https://storage.googleapis.com/golang/go${GOLANG_VERSION}.linux-amd64.tar.gz && \ 14 | tar -C /usr/local -xzf /opt/go${GOLANG_VERSION}.linux-amd64.tar.gz 15 | ENV PATH=$PATH:/usr/local/go/bin 16 | ENV GOPATH=/go 17 | 18 | # install 'dep' for Go package management 19 | RUN go get -u github.com/golang/dep/cmd/dep 20 | 21 | # import source from host 22 | ADD . /go/src/github.com/xcoulon/go-url-shortener 23 | WORKDIR /go/src/github.com/xcoulon/go-url-shortener 24 | RUN $GOPATH/bin/dep ensure -v 25 | 26 | # run the tests, using build args to specify the connection settings to the Postgres DB 27 | # optional args that can be filled with `build-arg` when executing the `docker build` command 28 | ARG POSTGRES_HOST 29 | ARG POSTGRES_PORT 30 | ARG POSTGRES_USER 31 | ARG POSTGRES_PASSWORD 32 | RUN LOG_LEVEL=debug go test ./... 33 | 34 | # build the application 35 | ARG BUILD_COMMIT=unknown 36 | ARG BUILD_TIME=unknown 37 | RUN go build -ldflags "-X github.com/xcoulon/go-url-shortener/configuration.BuildCommit=${BUILD_COMMIT} -X github.com/xcoulon/go-url-shortener/configuration.BuildTime=${BUILD_TIME}" -o bin/go-url-shortener 38 | 39 | # final image 40 | FROM centos:7 41 | LABEL author="Xavier Coulon " 42 | 43 | ARG BUILD_COMMIT=unknown 44 | ARG BUILD_TIME=unknown 45 | LABEL url-shortener.version=${BUILD_COMMIT} \ 46 | url-shortener.build-time=${BUILD_TIME} 47 | 48 | # Add the binary file generated in the `builder` container above 49 | COPY --from=builder /go/src/github.com/xcoulon/go-url-shortener/bin/go-url-shortener /usr/local/bin/go-url-shortener 50 | 51 | # Create a non-root user and a group with the same name: "shortenerapp" 52 | ENV USER_GROUP=shortenerapp 53 | RUN groupadd -r ${USER_GROUP} && \ 54 | useradd --no-create-home -g ${USER_GROUP} ${USER_GROUP} 55 | # From here onwards, any RUN, CMD, or ENTRYPOINT will be run under the following user instead of 'root' 56 | USER ${USER_GROUP} 57 | 58 | EXPOSE 8080 59 | 60 | ENTRYPOINT [ "/usr/local/bin/go-url-shortener" ] -------------------------------------------------------------------------------- /Dockerfile.openshift: -------------------------------------------------------------------------------- 1 | # build image 2 | FROM centos:7 3 | LABEL author="Xavier Coulon " 4 | ENV LANG=en_US.utf8 5 | 6 | # Install wget and git 7 | RUN yum --enablerepo=centosplus install -y \ 8 | wget \ 9 | git 10 | 11 | # install golang 1.10.1 12 | ENV GOLANG_VERSION=1.10.1 13 | RUN wget -O /opt/go${GOLANG_VERSION}.linux-amd64.tar.gz https://storage.googleapis.com/golang/go${GOLANG_VERSION}.linux-amd64.tar.gz && \ 14 | tar -C /usr/local -xzf /opt/go${GOLANG_VERSION}.linux-amd64.tar.gz 15 | ENV PATH=$PATH:/usr/local/go/bin 16 | ENV GOPATH=/go 17 | 18 | # install 'dep' for Go package management 19 | RUN go get -u github.com/golang/dep/cmd/dep 20 | 21 | # import source from host 22 | ADD . /go/src/github.com/xcoulon/go-url-shortener 23 | WORKDIR /go/src/github.com/xcoulon/go-url-shortener 24 | RUN $GOPATH/bin/dep ensure -v 25 | 26 | # run the tests, using build args to specify the connection settings to the Postgres DB 27 | # optional args that can be filled with `build-arg` when executing the `docker build` command 28 | #ARG POSTGRES_HOST 29 | #ARG POSTGRES_PORT 30 | #ARG POSTGRES_USER 31 | #ARG POSTGRES_PASSWORD 32 | #RUN LOG_LEVEL=debug go test ./... 33 | 34 | # build the application 35 | ARG BUILD_COMMIT=unknown 36 | ARG BUILD_TIME=unknown 37 | RUN go build -ldflags "-X github.com/xcoulon/go-url-shortener/configuration.BuildCommit=${BUILD_COMMIT} -X github.com/xcoulon/go-url-shortener/configuration.BuildTime=${BUILD_TIME}" -o bin/go-url-shortener 38 | 39 | LABEL url-shortener.version=${BUILD_COMMIT} \ 40 | url-shortener.build-time=${BUILD_TIME} 41 | 42 | # Add the binary file generated in the `builder` container above 43 | RUN mv /go/src/github.com/xcoulon/go-url-shortener/bin/go-url-shortener /usr/local/bin/go-url-shortener 44 | 45 | # Create a non-root user and a group with the same name: "shortenerapp" 46 | ENV USER_GROUP=shortenerapp 47 | RUN groupadd -r ${USER_GROUP} && \ 48 | useradd --no-create-home -g ${USER_GROUP} ${USER_GROUP} 49 | # From here onwards, any RUN, CMD, or ENTRYPOINT will be run under the following user instead of 'root' 50 | USER ${USER_GROUP} 51 | 52 | EXPOSE 8080 53 | 54 | ENTRYPOINT [ "/usr/local/bin/go-url-shortener" ] -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" 6 | name = "github.com/davecgh/go-spew" 7 | packages = ["spew"] 8 | pruneopts = "UT" 9 | revision = "346938d642f2ec3594ed81d874461961cd0faa76" 10 | version = "v1.1.0" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:fdae1c338ec6667687fb3fdbde842b3c421c930163981b5d441502b240b7f50b" 15 | name = "github.com/dchest/uniuri" 16 | packages = ["."] 17 | pruneopts = "UT" 18 | revision = "8902c56451e9b58ff940bbe5fec35d5f9c04584a" 19 | 20 | [[projects]] 21 | digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" 22 | name = "github.com/fsnotify/fsnotify" 23 | packages = ["."] 24 | pruneopts = "UT" 25 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" 26 | version = "v1.4.7" 27 | 28 | [[projects]] 29 | branch = "master" 30 | digest = "1:466594e2922a8ca66af07370ea29726c0d40272baaad1158a36cd94baa6367d1" 31 | name = "github.com/hashicorp/hcl" 32 | packages = [ 33 | ".", 34 | "hcl/ast", 35 | "hcl/parser", 36 | "hcl/printer", 37 | "hcl/scanner", 38 | "hcl/strconv", 39 | "hcl/token", 40 | "json/parser", 41 | "json/scanner", 42 | "json/token", 43 | ] 44 | pruneopts = "UT" 45 | revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" 46 | 47 | [[projects]] 48 | digest = "1:510a2b056950ed12b4f3ac704ee1b34fd1920cb5b26f60f98d069de89c60adb7" 49 | name = "github.com/jinzhu/gorm" 50 | packages = [ 51 | ".", 52 | "dialects/postgres", 53 | ] 54 | pruneopts = "UT" 55 | revision = "5174cc5c242a728b435ea2be8a2f7f998e15429b" 56 | version = "v1.0" 57 | 58 | [[projects]] 59 | digest = "1:5fc79df7c655911e3c5a1e7bce69c1956f50e07f1c7448678f6c1efdb9691597" 60 | name = "github.com/jinzhu/inflection" 61 | packages = ["."] 62 | pruneopts = "UT" 63 | revision = "74387dc39a75e970e7a3ae6a3386b5bd2e5c5cff" 64 | 65 | [[projects]] 66 | digest = "1:0f0dbabce22fdc4e4d752cd543c33b788e7c7a6e3ab48b54d449a22aa06922ee" 67 | name = "github.com/labstack/echo" 68 | packages = ["."] 69 | pruneopts = "UT" 70 | revision = "b338075a0fc6e1a0683dbf03d09b4957a289e26f" 71 | version = "3.2.6" 72 | 73 | [[projects]] 74 | digest = "1:d88036aa03a0d26d6e3e6f1a8304326112b5285b26a25081c6882055b7db45f0" 75 | name = "github.com/labstack/gommon" 76 | packages = [ 77 | "color", 78 | "log", 79 | ] 80 | pruneopts = "UT" 81 | revision = "57409ada9da0f2afad6664c49502f8c50fbd8476" 82 | version = "0.2.3" 83 | 84 | [[projects]] 85 | digest = "1:fa09bc1e3ac18618b207d6021234440ee2ae55f981d155db25bdf11c3aa18e7f" 86 | name = "github.com/lib/pq" 87 | packages = [ 88 | ".", 89 | "hstore", 90 | "oid", 91 | ] 92 | pruneopts = "UT" 93 | revision = "4a82388ebc5138c8289fe9bc602cb0b3e32cd617" 94 | 95 | [[projects]] 96 | digest = "1:737b7bc969e207193639c1f96c7b9d0ed8d6c5b5fa33fa3e0c2adb899a02e388" 97 | name = "github.com/magiconair/properties" 98 | packages = ["."] 99 | pruneopts = "UT" 100 | revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" 101 | version = "v1.7.4" 102 | 103 | [[projects]] 104 | branch = "master" 105 | digest = "1:f17bb51eb11a407870bd0e0d7223ac6194b3db11d1b32e0e569272427c78a8e7" 106 | name = "github.com/mattn/go-colorable" 107 | packages = ["."] 108 | pruneopts = "UT" 109 | revision = "6cc8b475d4682021d75d2cbe2bc481bec4ce98e5" 110 | 111 | [[projects]] 112 | branch = "master" 113 | digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5" 114 | name = "github.com/mattn/go-isatty" 115 | packages = ["."] 116 | pruneopts = "UT" 117 | revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" 118 | 119 | [[projects]] 120 | digest = "1:56a0bd1a1f3809171d1abe0bfd389558be0cd672e858e1f831b88f806aa8764f" 121 | name = "github.com/mitchellh/mapstructure" 122 | packages = ["."] 123 | pruneopts = "UT" 124 | revision = "06020f85339e21b2478f756a78e295255ffa4d6a" 125 | 126 | [[projects]] 127 | digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" 128 | name = "github.com/pelletier/go-toml" 129 | packages = ["."] 130 | pruneopts = "UT" 131 | revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" 132 | version = "v1.2.0" 133 | 134 | [[projects]] 135 | digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" 136 | name = "github.com/pkg/errors" 137 | packages = ["."] 138 | pruneopts = "UT" 139 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 140 | version = "v0.8.0" 141 | 142 | [[projects]] 143 | digest = "1:08413c4235cad94a96c39e1e2f697789733c4a87d1fdf06b412d2cf2ba49826a" 144 | name = "github.com/pmezard/go-difflib" 145 | packages = ["difflib"] 146 | pruneopts = "UT" 147 | revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d" 148 | 149 | [[projects]] 150 | digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925" 151 | name = "github.com/satori/uuid.go" 152 | packages = ["."] 153 | pruneopts = "UT" 154 | revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3" 155 | version = "v1.2.0" 156 | 157 | [[projects]] 158 | digest = "1:5622116f2c79239f2d25d47b881e14f96a8b8c17b63b8a8326a38ee1a332b007" 159 | name = "github.com/sirupsen/logrus" 160 | packages = ["."] 161 | pruneopts = "UT" 162 | revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" 163 | version = "v1.0.4" 164 | 165 | [[projects]] 166 | digest = "1:46c66b2046474cf473b116e5344fa7cf6aadce82941bf5dd49cf63a90b8dc31d" 167 | name = "github.com/spf13/afero" 168 | packages = [ 169 | ".", 170 | "mem", 171 | ] 172 | pruneopts = "UT" 173 | revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536" 174 | version = "v1.0.0" 175 | 176 | [[projects]] 177 | digest = "1:9b28ee2984c69d78afe2ce52b1650ba91a6381f355ff08c1d0e53d9e66bd62fe" 178 | name = "github.com/spf13/cast" 179 | packages = ["."] 180 | pruneopts = "UT" 181 | revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" 182 | version = "v1.1.0" 183 | 184 | [[projects]] 185 | digest = "1:9f28b7e326b8cd1db556678b7ce679cf9bf888647e40922f91d9d40f451f942b" 186 | name = "github.com/spf13/jwalterweatherman" 187 | packages = ["."] 188 | pruneopts = "UT" 189 | revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" 190 | 191 | [[projects]] 192 | branch = "master" 193 | digest = "1:772bf4d1907ccb275aaa532ec6cb0d85fc61bd05648af28551590af3ea4e2d53" 194 | name = "github.com/spf13/pflag" 195 | packages = ["."] 196 | pruneopts = "UT" 197 | revision = "4c012f6dcd9546820e378d0bdda4d8fc772cdfea" 198 | 199 | [[projects]] 200 | digest = "1:748519c76ecc7b5d673d7ee8924ace736ec1717b93011c77171b8bd961ac280c" 201 | name = "github.com/spf13/viper" 202 | packages = ["."] 203 | pruneopts = "UT" 204 | revision = "62edee319679b6ceaec16de03b966102d2dea709" 205 | 206 | [[projects]] 207 | digest = "1:4c03373575433ac188746ff25cc2b8b21331086d1c557e6e18088bba88f18fff" 208 | name = "github.com/stretchr/testify" 209 | packages = [ 210 | "assert", 211 | "require", 212 | ] 213 | pruneopts = "UT" 214 | revision = "b91bfb9ebec76498946beb6af7c0230c7cc7ba6c" 215 | version = "v1.2.0" 216 | 217 | [[projects]] 218 | branch = "master" 219 | digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59" 220 | name = "github.com/valyala/bytebufferpool" 221 | packages = ["."] 222 | pruneopts = "UT" 223 | revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" 224 | 225 | [[projects]] 226 | branch = "master" 227 | digest = "1:268b8bce0064e8c057d7b913605459f9a26dcab864c0886a56d196540fbf003f" 228 | name = "github.com/valyala/fasttemplate" 229 | packages = ["."] 230 | pruneopts = "UT" 231 | revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0" 232 | 233 | [[projects]] 234 | branch = "master" 235 | digest = "1:03cccc4702218a5f73400ec470b8c2921ff795165159d87fcf46457a06281f71" 236 | name = "golang.org/x/crypto" 237 | packages = [ 238 | "acme", 239 | "acme/autocert", 240 | "ssh/terminal", 241 | ] 242 | pruneopts = "UT" 243 | revision = "0efb9460aaf800c6376acf625be2853bceac2e06" 244 | 245 | [[projects]] 246 | digest = "1:0d38ee006fd127e3c539ef9eb0df6538462c62ab7b596d71bd7d581dc58c94f9" 247 | name = "golang.org/x/sys" 248 | packages = [ 249 | "unix", 250 | "windows", 251 | ] 252 | pruneopts = "UT" 253 | revision = "82aafbf43bf885069dc71b7e7c2f9d7a614d47da" 254 | 255 | [[projects]] 256 | digest = "1:49f6e78e8498938f529f3e0fec4b007efd670ae2008295409421227044d2139e" 257 | name = "golang.org/x/text" 258 | packages = [ 259 | "internal/gen", 260 | "internal/triegen", 261 | "internal/ucd", 262 | "transform", 263 | "unicode/cldr", 264 | "unicode/norm", 265 | ] 266 | pruneopts = "UT" 267 | revision = "88f656faf3f37f690df1a32515b479415e1a6769" 268 | 269 | [[projects]] 270 | digest = "1:c46d0fb245dbc49cb816b3e2818d1830e29d03bee1663b1e201f3ed918ec6e60" 271 | name = "gopkg.in/yaml.v2" 272 | packages = ["."] 273 | pruneopts = "UT" 274 | revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5" 275 | 276 | [solve-meta] 277 | analyzer-name = "dep" 278 | analyzer-version = 1 279 | input-imports = [ 280 | "github.com/dchest/uniuri", 281 | "github.com/fsnotify/fsnotify", 282 | "github.com/jinzhu/gorm", 283 | "github.com/jinzhu/gorm/dialects/postgres", 284 | "github.com/labstack/echo", 285 | "github.com/pelletier/go-toml", 286 | "github.com/pkg/errors", 287 | "github.com/satori/uuid.go", 288 | "github.com/sirupsen/logrus", 289 | "github.com/spf13/viper", 290 | "github.com/stretchr/testify/assert", 291 | "github.com/stretchr/testify/require", 292 | ] 293 | solver-name = "gps-cdcl" 294 | solver-version = 1 295 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | required = ["github.com/pelletier/go-toml"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/dchest/uniuri" 31 | 32 | [[constraint]] 33 | name = "github.com/fsnotify/fsnotify" 34 | version = "1.4.7" 35 | 36 | [[constraint]] 37 | name = "github.com/jinzhu/gorm" 38 | version = "1.0.0" 39 | 40 | [[constraint]] 41 | name = "github.com/labstack/echo" 42 | version = "3.2.3" 43 | 44 | [[constraint]] 45 | name = "github.com/pkg/errors" 46 | version = "0.8.0" 47 | 48 | [[constraint]] 49 | name = "github.com/satori/uuid.go" 50 | version = "1.1.0" 51 | 52 | [[constraint]] 53 | name = "github.com/sirupsen/logrus" 54 | version = "1.0.3" 55 | 56 | [[constraint]] 57 | name = "github.com/spf13/viper" 58 | revision = "62edee319679b6ceaec16de03b966102d2dea709" 59 | 60 | [[constraint]] 61 | name = "github.com/pelletier/go-toml" 62 | version = "v1.2.0" 63 | 64 | [[constraint]] 65 | name = "github.com/stretchr/testify" 66 | version = "1.1.4" 67 | 68 | [prune] 69 | go-tests = true 70 | unused-packages = true 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If nothing was specified, show help 2 | .PHONY: help 3 | # Based on https://gist.github.com/rcmachado/af3db315e31383502660 4 | ## Display this help text. 5 | help:/ 6 | $(info Available targets) 7 | $(info -----------------) 8 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 9 | helpMessage = match(lastLine, /^## (.*)/); \ 10 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 11 | if (helpMessage) { \ 12 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 13 | gsub(/##/, "\n ", helpMessage); \ 14 | printf "%-35s - %s\n", helpCommand, helpMessage; \ 15 | lastLine = "" \ 16 | } \ 17 | } \ 18 | { hasComment = match(lastLine, /^## (.*)/); \ 19 | if(hasComment) { \ 20 | lastLine=lastLine$$0; \ 21 | } \ 22 | else { \ 23 | lastLine = $$0 \ 24 | } \ 25 | }' $(MAKEFILE_LIST) 26 | 27 | POSTGRES_PASSWORD:=mysecretpassword 28 | UNAME_S := $(shell uname -s) 29 | OC_USERNAME := developer 30 | OC_PASSWORD := developer 31 | MINISHIFT_IP := $(shell minishift ip) 32 | PROJECT_NAME := sandbox 33 | OC_PROJECT=$(PROJECT_NAME) 34 | REGISTRY_URI := $(shell minishift openshift registry) 35 | REGISTRY_IMAGE = webapp 36 | 37 | .PHONY: init 38 | init: 39 | @rm -rf .tmp 40 | @mkdir .tmp 41 | 42 | 43 | .PHONY: clean 44 | clean: 45 | @rm -rf .tmp 46 | 47 | .PHONY: minishift-login 48 | ## login to minishift 49 | minishift-login: 50 | @echo "Login to minishift..." 51 | @oc login --insecure-skip-tls-verify=true https://$(MINISHIFT_IP):8443 -u developer -p developer 1>/dev/null 52 | 53 | .PHONY: minishift-deploy 54 | ## deploy the image on minishift 55 | minishift-deploy: minishift-login 56 | eval $$(minishift docker-env) && docker login -u developer -p $(shell oc whoami -t) $(shell minishift openshift registry) && docker tag ${OC_PROJECT}/${REGISTRY_IMAGE}:latest ${REGISTRY_URI}/${OC_PROJECT}/${REGISTRY_IMAGE}:latest 57 | eval $$(minishift docker-env) && docker login -u developer -p $(shell oc whoami -t) $(shell minishift openshift registry) && docker push $(shell minishift openshift registry)/${OC_PROJECT}/${REGISTRY_IMAGE}:latest 58 | 59 | .PHONY: minishift-image 60 | ## build image on minishift 61 | minishift-image: 62 | echo "building the application image..." 63 | $(eval BUILD_COMMIT:=$(shell git rev-parse --short HEAD)) 64 | $(eval BUILD_TIME:=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ')) 65 | eval $$(minishift docker-env) && \ 66 | docker build --build-arg POSTGRES_HOST=`cat .tmp/postgres.host` \ 67 | --build-arg BUILD_COMMIT=$(BUILD_COMMIT) \ 68 | --build-arg BUILD_TIME=$(BUILD_TIME) \ 69 | --file Dockerfile.openshift \ 70 | . \ 71 | -t ${OC_PROJECT}/$(REGISTRY_IMAGE):$(BUILD_COMMIT) && \ 72 | docker tag ${OC_PROJECT}/$(REGISTRY_IMAGE):$(BUILD_COMMIT) ${OC_PROJECT}/$(REGISTRY_IMAGE):latest 73 | 74 | .PHONY: start-db 75 | start-db: init 76 | @echo "starting the test db container..." 77 | ifeq ($(UNAME_S),Darwin) 78 | @echo "docker.for.mac.host.internal" > .tmp/postgres.host 79 | else 80 | @echo "localhost" > .tmp/postgres.host 81 | endif 82 | docker run -P -d --cidfile .tmp/postgres.cid -e POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) postgres:10.1 > /dev/null 83 | docker inspect `cat .tmp/postgres.cid` \ 84 | --format='{{ with index .NetworkSettings.Ports "5432/tcp" }}{{ with index . 0 }}{{ index . "HostPort" }}{{ end }}{{ end }}' \ 85 | > .tmp/postgres.port 86 | @echo "test db instance is listening on `cat .tmp/postgres.host`:`cat .tmp/postgres.port`" 87 | 88 | .PHONY: build 89 | build: start-db 90 | @echo "building the application image..." 91 | $(eval BUILD_COMMIT:=$(shell git rev-parse --short HEAD)) 92 | $(eval BUILD_TIME:=$(shell date -u '+%Y-%m-%dT%H:%M:%SZ')) 93 | docker build --build-arg POSTGRES_HOST=`cat .tmp/postgres.host` \ 94 | --build-arg POSTGRES_PORT=`cat .tmp/postgres.port` \ 95 | --build-arg POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) \ 96 | --build-arg BUILD_COMMIT=$(BUILD_COMMIT) \ 97 | --build-arg BUILD_TIME=$(BUILD_TIME) \ 98 | --file Dockerfile.openshift \ 99 | . \ 100 | -t $(REGISTRY_IMAGE):$(BUILD_COMMIT) 101 | docker tag $(REGISTRY_IMAGE):$(BUILD_COMMIT) $(REGISTRY_IMAGE):latest 102 | 103 | .PHONY: kill-db 104 | kill-db: 105 | @echo "killing the test db container..." 106 | docker rm -f `cat .tmp/postgres.cid` -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = go-url-shortener 2 | 3 | Sample URL shortener written in Golang, to deploy on Kubernetes/Minikube by following the instructions in https://medium.com/@xcoulon/deploying-your-first-web-app-on-minikube-6e98d2884b3a[this article]. 4 | 5 | License: Apache License 2.0 6 | 7 | == Deployment on OpenShift 8 | 9 | run the following commands: 10 | 11 | ``` 12 | # deploy the database secrets (credentials) 13 | oc apply -f templates/database-secrets.yml 14 | # deploy the database (postgres 10) 15 | oc apply -f templates/database-deployment.openshift.yml 16 | # deploy the internal service 17 | oc apply -f templates/database-service.yml 18 | 19 | # create the web app config 20 | oc apply -f templates/webapp-config.yaml 21 | # create a deployment config for the web app 22 | oc apply -f templates/webapp-deploymentconfig.yml 23 | # expose the webapp as a node service 24 | oc apply -f templates/webapp-service.yml 25 | 26 | # rollout the latest version of the webapp (if an image already exists in the container registry) 27 | oc rollout latest deploymentconfigs/webapp 28 | 29 | ``` 30 | 31 | Once the deployment config is in place, just deploy a new version of the webapp with `make minishift-image && make push-minishift` 32 | 33 | == TODO 34 | 35 | - see howe to run the tests during the build 36 | - see how to PVC support for the storage, which fail for now with the following error (when running with the `developer` account): 37 | ``` 38 | > oc apply -f templates/database-storage.yml 39 | persistentvolumeclaim "postgres-pv-claim" unchanged 40 | Error from server (Forbidden): error when retrieving current configuration of: 41 | &{0xc420d9aa80 0xc420aa01c0 postgres-pv templates/database-storage.yml 0xc4213e30b0 false} 42 | from server for: "templates/database-storage.yml": persistentvolumes "postgres-pv" is forbidden: User "developer" cannot get persistentvolumes at the cluster scope: User "developer" cannot get persistentvolumes at the cluster scope 43 | ``` 44 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | log.level: info 2 | -------------------------------------------------------------------------------- /configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/fsnotify/fsnotify" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var ( 15 | // BuildCommit lastest build commit (set by build script) 16 | BuildCommit = "unknown" 17 | // BuildTime set by build script 18 | BuildTime = "unknown" 19 | ) 20 | 21 | const ( 22 | // Constants for viper variable names. Will be used to set 23 | // default values as well as to get each value 24 | varPostgresHost = "postgres.host" 25 | varPostgresPort = "postgres.port" 26 | varPostgresDatabase = "postgres.database" 27 | varPostgresUser = "postgres.user" 28 | varPostgresPassword = "postgres.password" 29 | varPostgresSuperUser = "postgres.superuser" 30 | varPostgresAdminPassword = "postgres.admin.password" 31 | varPostgresSSLMode = "postgres.sslmode" 32 | varPostgresConnectionTimeout = "postgres.connection.timeout" 33 | varPostgresTransactionTimeout = "postgres.transaction.timeout" 34 | varPostgresConnectionRetrySleep = "postgres.connection.retrysleep" 35 | varPostgresConnectionMaxIdle = "postgres.connection.maxidle" 36 | varPostgresConnectionMaxOpen = "postgres.connection.maxopen" 37 | varPathToConfig = "config.file" 38 | varLogLevel = "log.level" 39 | ) 40 | 41 | // Configuration the application Configuration, based on ENV variables 42 | type Configuration struct { 43 | v *viper.Viper 44 | } 45 | 46 | // New initializes a new Configuration from the ENV variables 47 | func New() *Configuration { 48 | c := Configuration{ 49 | v: viper.New(), 50 | } 51 | c.v.SetDefault(varPostgresHost, "localhost") 52 | c.v.SetDefault(varPostgresPort, 5432) 53 | c.v.SetDefault(varPostgresDatabase, "postgres") 54 | c.v.SetDefault(varPostgresUser, "postgres") 55 | c.v.SetDefault(varPostgresSuperUser, "postgres") 56 | c.v.SetDefault(varPostgresSSLMode, "disable") 57 | c.v.SetDefault(varPostgresConnectionTimeout, 5) 58 | c.v.SetDefault(varPostgresConnectionMaxIdle, -1) 59 | c.v.SetDefault(varPostgresConnectionMaxOpen, -1) 60 | c.v.SetDefault(varPostgresConnectionRetrySleep, time.Duration(time.Second)) 61 | c.v.SetDefault(varPostgresTransactionTimeout, time.Duration(5*time.Minute)) 62 | c.v.SetDefault(varPathToConfig, "./config.yaml") 63 | c.v.AutomaticEnv() 64 | c.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 65 | c.v.SetTypeByDefaultValue(true) 66 | c.v.SetDefault(varLogLevel, "info") 67 | c.v.SetConfigFile(c.GetPathToConfig()) 68 | c.v.SetTypeByDefaultValue(true) 69 | err := c.v.ReadInConfig() // Find and read the config file 70 | logrus.WithField("path", c.GetPathToConfig()).Warn("loading config") 71 | // just use the default value(s) if the config file was not found 72 | if _, ok := err.(*os.PathError); ok { 73 | logrus.Warnf("no config file '%s' not found. Using default values", c.GetPathToConfig()) 74 | } else if err != nil { // Handle other errors that occurred while reading the config file 75 | panic(fmt.Errorf("fatal error while reading the config file: %s", err)) 76 | } 77 | setLogLevel(c.GetLogLevel()) 78 | // monitor the changes in the config file 79 | c.v.WatchConfig() 80 | c.v.OnConfigChange(func(e fsnotify.Event) { 81 | logrus.WithField("file", e.Name).Warn("Config file changed") 82 | setLogLevel(c.GetLogLevel()) 83 | }) 84 | return &c 85 | } 86 | 87 | func setLogLevel(logLevel string) { 88 | logrus.WithField("level", logLevel).Warn("setting log level") 89 | level, err := logrus.ParseLevel(logLevel) 90 | if err != nil { 91 | logrus.WithField("level", logLevel).Fatalf("failed to start: %s", err.Error()) 92 | } 93 | logrus.SetLevel(level) 94 | 95 | } 96 | 97 | // GetPostgresHost returns the postgres host as set via default, config file, or environment variable 98 | func (c *Configuration) GetPostgresHost() string { 99 | return c.v.GetString(varPostgresHost) 100 | } 101 | 102 | // GetPostgresPort returns the postgres port as set via default, config file, or environment variable 103 | func (c *Configuration) GetPostgresPort() int64 { 104 | return c.v.GetInt64(varPostgresPort) 105 | } 106 | 107 | // GetPostgresDatabase returns the postgres database as set via default, config file, or environment variable 108 | func (c *Configuration) GetPostgresDatabase() string { 109 | return c.v.GetString(varPostgresDatabase) 110 | } 111 | 112 | // GetPostgresUser returns the postgres user as set via default, config file, or environment variable 113 | func (c *Configuration) GetPostgresUser() string { 114 | return c.v.GetString(varPostgresUser) 115 | } 116 | 117 | // GetPostgresPassword returns the postgres password as set via default, config file, or environment variable 118 | func (c *Configuration) GetPostgresPassword() string { 119 | return c.v.GetString(varPostgresPassword) 120 | } 121 | 122 | // GetPostgresSuperUser returns the postgres superuser as set via default, config file, or environment variable 123 | func (c *Configuration) GetPostgresSuperUser() string { 124 | return c.v.GetString(varPostgresSuperUser) 125 | } 126 | 127 | // GetPostgresAdminPassword returns the postgres password as set via default, config file, or environment variable 128 | func (c *Configuration) GetPostgresAdminPassword() string { 129 | return c.v.GetString(varPostgresAdminPassword) 130 | } 131 | 132 | // GetPostgresSSLMode returns the postgres sslmode as set via default, config file, or environment variable 133 | func (c *Configuration) GetPostgresSSLMode() string { 134 | return c.v.GetString(varPostgresSSLMode) 135 | } 136 | 137 | // GetPostgresConnectionTimeout returns the postgres connection timeout as set via default, config file, or environment variable 138 | func (c *Configuration) GetPostgresConnectionTimeout() int64 { 139 | return c.v.GetInt64(varPostgresConnectionTimeout) 140 | } 141 | 142 | // GetPostgresConnectionRetrySleep returns the number of seconds (as set via default, config file, or environment variable) 143 | // to wait before trying to connect again 144 | func (c *Configuration) GetPostgresConnectionRetrySleep() time.Duration { 145 | return c.v.GetDuration(varPostgresConnectionRetrySleep) 146 | } 147 | 148 | // GetPostgresTransactionTimeout returns the number of minutes to timeout a transaction 149 | func (c *Configuration) GetPostgresTransactionTimeout() time.Duration { 150 | return c.v.GetDuration(varPostgresTransactionTimeout) 151 | } 152 | 153 | // GetPostgresConnectionMaxIdle returns the number of connections that should be keept alive in the database connection pool at 154 | // any given time. -1 represents no restrictions/default behavior 155 | func (c *Configuration) GetPostgresConnectionMaxIdle() int { 156 | return c.v.GetInt(varPostgresConnectionMaxIdle) 157 | } 158 | 159 | // GetPostgresConnectionMaxOpen returns the max number of open connections that should be open in the database connection pool. 160 | // -1 represents no restrictions/default behavior 161 | func (c *Configuration) GetPostgresConnectionMaxOpen() int { 162 | return c.v.GetInt(varPostgresConnectionMaxOpen) 163 | } 164 | 165 | //GetPostgresConfig returns the settings for opening a new connection on a PostgreSQL server 166 | func (c *Configuration) GetPostgresConfig() string { 167 | return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=%d", 168 | c.GetPostgresHost(), 169 | c.GetPostgresPort(), 170 | c.GetPostgresUser(), 171 | c.GetPostgresPassword(), 172 | c.GetPostgresDatabase(), 173 | c.GetPostgresSSLMode(), 174 | c.GetPostgresConnectionTimeout()) 175 | } 176 | 177 | //GetPostgresAdminConfig returns the settings for opening a new connection on a PostgreSQL server 178 | func (c *Configuration) GetPostgresAdminConfig() string { 179 | return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=%d", 180 | c.GetPostgresHost(), 181 | c.GetPostgresPort(), 182 | c.GetPostgresSuperUser(), 183 | c.GetPostgresAdminPassword(), 184 | c.GetPostgresDatabase(), 185 | c.GetPostgresSSLMode(), 186 | c.GetPostgresConnectionTimeout()) 187 | } 188 | 189 | // GetPathToConfig returns the path to the config file 190 | func (c *Configuration) GetPathToConfig() string { 191 | return c.v.GetString(varPathToConfig) 192 | } 193 | 194 | // GetLogLevel returns the log level 195 | func (c *Configuration) GetLogLevel() string { 196 | return c.v.GetString(varLogLevel) 197 | } 198 | -------------------------------------------------------------------------------- /connection/connection.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/pkg/errors" 6 | "github.com/sirupsen/logrus" 7 | "github.com/xcoulon/go-url-shortener/configuration" 8 | ) 9 | 10 | // NewUserConnection returns a new database connection. 11 | func NewUserConnection(config *configuration.Configuration) (*gorm.DB, error) { 12 | logrus.Infof("Connecting to Postgres database using: host=`%s:%d` dbname=`%s` username=`%s`", 13 | config.GetPostgresHost(), config.GetPostgresPort(), config.GetPostgresDatabase(), config.GetPostgresUser()) 14 | db, err := gorm.Open("postgres", config.GetPostgresConfig()) 15 | if err != nil { 16 | return nil, errors.Wrap(err, "failed to open connection to database") 17 | } 18 | return db, nil 19 | } 20 | 21 | // SetupUUIDExtension setup the extension to use UUID, which require superuse privileges 22 | func SetupUUIDExtension(config *configuration.Configuration) error { 23 | logrus.Infof("Connecting to Postgres database using: host=`%s:%d` dbname=`%s` username=`%s`", 24 | config.GetPostgresHost(), config.GetPostgresPort(), config.GetPostgresDatabase(), config.GetPostgresSuperUser()) 25 | db, err := gorm.Open("postgres", config.GetPostgresAdminConfig()) 26 | if err != nil { 27 | return errors.Wrap(err, "failed to open connection to database") 28 | } 29 | // ensure that the Postgres DB has the "uuid-ossp" extension to generate UUIDs as the primary keys for the ShortenedURL records 30 | logrus.Info(`Adding the 'uuid-ossp' extension...`) 31 | err = db.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`).Error 32 | if err != nil { 33 | return errors.Wrap(err, "failed to setup the database") 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/xcoulon/go-url-shortener/configuration" 10 | "github.com/xcoulon/go-url-shortener/connection" 11 | "github.com/xcoulon/go-url-shortener/server" 12 | "github.com/xcoulon/go-url-shortener/storage" 13 | 14 | "github.com/jinzhu/gorm" 15 | ) 16 | 17 | func main() { 18 | // load the logging configuration and init the logger 19 | // logrus.SetFormatter(&logrus.JSONFormatter{}) 20 | 21 | config := configuration.New() 22 | // load the configuration and init the storage 23 | err := connection.SetupUUIDExtension(config) 24 | if err != nil { 25 | logrus.Fatalf("failed to start: %s", err.Error()) 26 | } 27 | // load the configuration and init the storage 28 | db, err := connection.NewUserConnection(config) 29 | if err != nil { 30 | logrus.Fatalf("failed to start: %s", err.Error()) 31 | } 32 | repository := storage.New(db) 33 | // handle shutdown 34 | go handleShutdown(db) 35 | s := server.New(repository) 36 | // listen and serve on 0.0.0.0:8080 37 | s.Start(":8080") 38 | } 39 | 40 | func handleShutdown(db *gorm.DB) { 41 | c := make(chan os.Signal) 42 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 43 | <-c 44 | // handle ctrl+c event here 45 | // for example, close database 46 | logrus.Warn("Closing DB connection before complete shutdown") 47 | err := db.Close() 48 | if err != nil { 49 | logrus.Errorf("error while closing the connection to the database: %v", err) 50 | } 51 | os.Exit(0) 52 | } 53 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo" 9 | "github.com/sirupsen/logrus" 10 | "github.com/xcoulon/go-url-shortener/configuration" 11 | "github.com/xcoulon/go-url-shortener/storage" 12 | ) 13 | 14 | // New instanciates a new Echo server 15 | func New(repository *storage.Repository) *echo.Echo { 16 | // starts the HTTP engine to handle requests 17 | e := echo.New() 18 | // graceful handle of errors, i.e., just logging with the same logger as everywhere else in the app. 19 | e.HTTPErrorHandler = func(err error, c echo.Context) { 20 | if he, ok := err.(*echo.HTTPError); ok { 21 | logrus.WithField("code", he.Code).Error(he.Message) 22 | if msg, ok := he.Message.(string); ok { 23 | c.String(he.Code, msg) 24 | } else { 25 | c.NoContent(he.Code) 26 | } 27 | } 28 | } 29 | e.POST("/", CreateURL(repository)) 30 | e.GET("/:shortURL", RetrieveURL(repository)) 31 | e.GET("/status", Status()) 32 | return e 33 | } 34 | 35 | // Status returns a basic `ping/pong` handler 36 | func Status() echo.HandlerFunc { 37 | return func(c echo.Context) error { 38 | return c.String(http.StatusOK, fmt.Sprintf("build.time: %s - build.commit: %s 👷‍♂️", configuration.BuildTime, configuration.BuildCommit)) 39 | } 40 | } 41 | 42 | // CreateURL returns a handler to create an db record from the `full_url` form param of the request. 43 | func CreateURL(repository *storage.Repository) echo.HandlerFunc { 44 | return func(c echo.Context) error { 45 | scheme := c.Scheme() 46 | host := c.Request().Host 47 | logrus.Debugf("Processing incoming request on %s://%s%s", scheme, host, c.Request().URL) 48 | fullURL := c.FormValue("full_url") 49 | if fullURL == "" { 50 | return echo.NewHTTPError(http.StatusBadRequest, "missing `full_url` form param in request") 51 | } 52 | shortURL, err := repository.Create(fullURL) 53 | if err != nil { 54 | logrus.Errorf("failed to store url: %v", err) 55 | return echo.NewHTTPError(http.StatusInternalServerError, "failed to store URL") 56 | } 57 | location := fmt.Sprintf("%s://%s/%s", scheme, host, *shortURL) 58 | logrus.Infof("Generated location for further usage: %s", location) 59 | c.Response().Header().Set(echo.HeaderLocation, location) 60 | c.String(http.StatusCreated, location) 61 | return nil 62 | } 63 | } 64 | 65 | // RetrieveURL returns a handler that retrieves the full URL from the `shortURL` request param. 66 | // Returns a `Temporary Redirect` response with the result or `Not Found` if no match was found. 67 | func RetrieveURL(repository *storage.Repository) echo.HandlerFunc { 68 | return func(c echo.Context) error { 69 | shortURL := c.Param("shortURL") 70 | fullURL, err := repository.Lookup(shortURL) 71 | if err != nil { 72 | logrus.Errorf("failed to retrieve url: %v", err) 73 | return echo.NewHTTPError(http.StatusInternalServerError, "failed to retrieve URL") 74 | } else if fullURL == nil { 75 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No record found for '%s'", shortURL)) 76 | } 77 | var result string 78 | if !strings.HasPrefix(*fullURL, "http://") && !strings.HasPrefix(*fullURL, "https://") { 79 | result = "http://" + *fullURL 80 | } else { 81 | result = *fullURL 82 | } 83 | c.Response().Header().Set(echo.HeaderLocation, result) 84 | c.Response().WriteHeader(http.StatusTemporaryRedirect) 85 | return nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server_test 2 | 3 | import ( 4 | "net/http/httptest" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/labstack/echo" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/xcoulon/go-url-shortener/configuration" 12 | "github.com/xcoulon/go-url-shortener/connection" 13 | "github.com/xcoulon/go-url-shortener/server" 14 | "github.com/xcoulon/go-url-shortener/storage" 15 | ) 16 | 17 | func TestServer(t *testing.T) { 18 | config := configuration.New() 19 | db, err := connection.NewUserConnection(config) 20 | require.Nil(t, err) 21 | repository := storage.New(db) 22 | s := server.New(repository) 23 | 24 | t.Run("status", func(t *testing.T) { 25 | // given 26 | req := httptest.NewRequest(echo.GET, "/status", nil) 27 | rec := httptest.NewRecorder() 28 | // when 29 | s.ServeHTTP(rec, req) 30 | // then 31 | assert.Equal(t, 200, rec.Code) 32 | }) 33 | 34 | t.Run("POST and GET", func(t *testing.T) { 35 | // given 36 | req1 := httptest.NewRequest(echo.POST, "http://localhost:8080/", strings.NewReader("full_url=http://redhat.com")) 37 | req1.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) 38 | rec1 := httptest.NewRecorder() 39 | // when 40 | s.ServeHTTP(rec1, req1) 41 | // then 42 | require.Equal(t, 201, rec1.Code) 43 | require.NotNil(t, rec1.Header()[echo.HeaderLocation]) 44 | location := rec1.Header()[echo.HeaderLocation][0] 45 | // given 46 | 47 | req2 := httptest.NewRequest(echo.GET, location, nil) 48 | rec2 := httptest.NewRecorder() 49 | // when 50 | s.ServeHTTP(rec2, req2) 51 | // then 52 | require.Equal(t, 307, rec2.Code) 53 | require.NotNil(t, rec2.Header()[echo.HeaderLocation]) 54 | assert.Equal(t, "http://redhat.com", rec2.Header()[echo.HeaderLocation][0]) 55 | }) 56 | 57 | t.Run("GET unknown", func(t *testing.T) { 58 | // given 59 | req := httptest.NewRequest(echo.GET, "http://localhost:8080/foo", nil) 60 | rec := httptest.NewRecorder() 61 | // when 62 | s.ServeHTTP(rec, req) 63 | // then 64 | require.Equal(t, 404, rec.Code) 65 | require.Nil(t, rec.Header()[echo.HeaderLocation]) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /storage/repository.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/dchest/uniuri" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/postgres" 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Repository the repository structure to create and retrieve Shortened URLs 12 | type Repository struct { 13 | db *gorm.DB 14 | } 15 | 16 | // New returns a new repository configured with the given 'db' 17 | func New(db *gorm.DB) *Repository { 18 | db.AutoMigrate(&ShortenedURL{}) 19 | return &Repository{db: db} 20 | } 21 | 22 | //Create creates a new entry 23 | func (r *Repository) Create(fullURL string) (*string, error) { 24 | shortURL := uniuri.NewLen(7) 25 | s := ShortenedURL{ 26 | LongURL: fullURL, 27 | ShortURL: shortURL, 28 | } 29 | err := r.db.Create(&s).Error 30 | if err != nil { 31 | return nil, errors.Wrapf(err, "failed to create new shortened URL record in DB") 32 | } 33 | logrus.WithField("long_url", s.LongURL).WithField("short_url", s.ShortURL).Infof("Stored URL in DB") 34 | return &shortURL, nil 35 | } 36 | 37 | //Lookup looks-up an entry in the DB 38 | func (r *Repository) Lookup(shortURL string) (*string, error) { 39 | logrus.WithField("short_url", shortURL).Debug("Looking-up by short_url in DB") 40 | var record ShortenedURL 41 | result := r.db.Where("short_url = ?", shortURL).First(&record) 42 | if result.RecordNotFound() { 43 | logrus.WithField("short_url", shortURL).Warn("No entry for short_url in DB") 44 | return nil, nil 45 | } else if result.Error != nil { 46 | return nil, errors.Wrapf(result.Error, "failed to look-up shortened URL record in DB") 47 | } 48 | return &record.LongURL, nil 49 | } 50 | -------------------------------------------------------------------------------- /storage/url.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/satori/uuid.go" 7 | ) 8 | 9 | // ShortenedURL the structure for shortened URLs 10 | type ShortenedURL struct { 11 | ID uuid.UUID `sql:"type:uuid default uuid_generate_v4()" gorm:"primary_key"` 12 | CreatedAt time.Time 13 | LongURL string 14 | ShortURL string 15 | } 16 | 17 | //TableName set ShortenedURL's table name to be `urls` 18 | func (ShortenedURL) TableName() string { 19 | return "urls" 20 | } 21 | -------------------------------------------------------------------------------- /templates/database-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: postgres 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | app: postgres 10 | spec: 11 | containers: 12 | - name: postgres 13 | image: centos/postgresql-10-centos7 14 | ports: 15 | - containerPort: 5432 16 | env: 17 | - name: POSTGRESQL_DATABASE 18 | valueFrom: 19 | secretKeyRef: 20 | name: database-secret-config 21 | key: dbname 22 | - name: POSTGRESQL_USER 23 | valueFrom: 24 | secretKeyRef: 25 | name: database-secret-config 26 | key: username 27 | - name: POSTGRESQL_PASSWORD 28 | valueFrom: 29 | secretKeyRef: 30 | name: database-secret-config 31 | key: password 32 | - name: POSTGRESQL_ADMIN_PASSWORD 33 | valueFrom: 34 | secretKeyRef: 35 | name: database-secret-config 36 | key: admin_password 37 | # volumeMounts: 38 | # - mountPath: /var/lib/postgresql/data 39 | # name: postgres-pv-claim 40 | # volumes: 41 | # - name: postgres-pv-claim 42 | # persistentVolumeClaim: 43 | # claimName: postgres-pv-claim 44 | -------------------------------------------------------------------------------- /templates/database-secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: database-secret-config 5 | type: Opaque 6 | data: 7 | dbname: dXJsX3Nob3J0ZW5lcl9kYg== 8 | username: dXNlcg== 9 | password: bXlzZWNyZXRwYXNzd29yZA== 10 | admin_password: bXlzZWNyZXRwYXNzd29yZA== -------------------------------------------------------------------------------- /templates/database-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres 5 | spec: 6 | ports: 7 | - port: 5432 8 | selector: 9 | app: postgres 10 | -------------------------------------------------------------------------------- /templates/database-storage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: PersistentVolume 3 | apiVersion: v1 4 | metadata: 5 | name: postgres-pv 6 | labels: 7 | type: local 8 | spec: 9 | storageClassName: manual 10 | capacity: 11 | storage: 100M 12 | accessModes: 13 | - ReadWriteOnce 14 | hostPath: 15 | path: "/mnt/data" 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolumeClaim 19 | metadata: 20 | labels: 21 | app: postgres 22 | name: postgres-pv-claim 23 | spec: 24 | storageClassName: manual 25 | accessModes: 26 | - ReadWriteOnce 27 | resources: 28 | requests: 29 | storage: 100M 30 | -------------------------------------------------------------------------------- /templates/namespace.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: sandbox 5 | -------------------------------------------------------------------------------- /templates/webapp-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | data: 4 | config.yaml: 'log.level: debug' 5 | metadata: 6 | name: app-config -------------------------------------------------------------------------------- /templates/webapp-deploymentconfig.yml: -------------------------------------------------------------------------------- 1 | kind: "DeploymentConfig" 2 | apiVersion: "v1" 3 | metadata: 4 | name: "webapp" 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | app: webapp 10 | spec: 11 | volumes: 12 | - name: config-volume 13 | configMap: 14 | name: app-config 15 | initContainers: 16 | - name: check-db-ready 17 | image: centos/postgresql-10-centos7 18 | command: ['sh', '-c', "-i", 19 | 'until pg_isready -h postgres -p 5432; 20 | do echo waiting for database; sleep 2; done;'] 21 | 22 | containers: 23 | - name: webapp 24 | image: 172.30.1.1:5000/sandbox/webapp:latest 25 | volumeMounts: 26 | - name: config-volume 27 | mountPath: /etc/config 28 | ports: 29 | - containerPort: 8080 30 | env: 31 | - name: CONFIG_FILE 32 | value: /etc/config/config.yaml 33 | - name: POSTGRES_HOST 34 | value: postgres 35 | - name: POSTGRES_PORT 36 | value: "5432" 37 | - name: POSTGRES_DATABASE 38 | valueFrom: 39 | secretKeyRef: 40 | name: database-secret-config 41 | key: dbname 42 | - name: POSTGRES_USER 43 | valueFrom: 44 | secretKeyRef: 45 | name: database-secret-config 46 | key: username 47 | - name: POSTGRES_PASSWORD 48 | valueFrom: 49 | secretKeyRef: 50 | name: database-secret-config 51 | key: password 52 | - name: POSTGRES_ADMIN_PASSWORD 53 | valueFrom: 54 | secretKeyRef: 55 | name: database-secret-config 56 | key: admin_password 57 | replicas: 1 58 | triggers: 59 | - type: "ConfigChange" 60 | - type: "ImageChange" 61 | imageChangeParams: 62 | automatic: true 63 | containerNames: 64 | - "webapp" 65 | from: 66 | kind: "ImageStreamTag" 67 | name: "webapp:latest" 68 | strategy: 69 | type: "Rolling" 70 | paused: false 71 | revisionHistoryLimit: 5 72 | minReadySeconds: 0 -------------------------------------------------------------------------------- /templates/webapp-imagestream.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: webapp -------------------------------------------------------------------------------- /templates/webapp-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webapp 5 | spec: 6 | type: NodePort 7 | ports: 8 | - nodePort: 31317 9 | port: 8080 10 | protocol: TCP 11 | targetPort: 8080 12 | selector: 13 | app: webapp 14 | --------------------------------------------------------------------------------