OpenShift/K8S Operator (#3)
* Vendor dependencies * OpenShift/K8S Operator * Disable ssl for realm by default * Fix ssl for realms. Create view role6.18.x
parent
63d9133fd1
commit
914e92e4e6
|
|
@ -0,0 +1,24 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
# This program and the accompanying materials are made
|
||||||
|
# available under the terms of the Eclipse Public License 2.0
|
||||||
|
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: EPL-2.0
|
||||||
|
#
|
||||||
|
# Contributors:
|
||||||
|
# Red Hat, Inc. - initial API and implementation
|
||||||
|
|
||||||
|
FROM golang:1.10.3 as builder
|
||||||
|
ADD . /go/src/github.com/eclipse/che-operator
|
||||||
|
RUN cd /go/src/github.com/eclipse/che-operator && go test -v ./...
|
||||||
|
RUN OOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||||
|
go build -o /tmp/che-operator/che-operator \
|
||||||
|
/go/src/github.com/eclipse/che-operator/cmd/che-operator/main.go
|
||||||
|
|
||||||
|
FROM alpine:3.7
|
||||||
|
COPY --from=builder /tmp/che-operator/che-operator /usr/local/bin/che-operator
|
||||||
|
COPY --from=builder /go/src/github.com/eclipse/che-operator/deploy/keycloak_provision /tmp/keycloak_provision
|
||||||
|
RUN adduser -D che-operator
|
||||||
|
USER che-operator
|
||||||
|
CMD ["che-operator"]
|
||||||
|
|
@ -0,0 +1,723 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:5c3894b2aa4d6bead0ceeea6831b305d62879c871780e7b76296ded1b004bc57"
|
||||||
|
name = "cloud.google.com/go"
|
||||||
|
packages = ["compute/metadata"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "97efc2c9ffd9fe8ef47f7f3203dc60bbca547374"
|
||||||
|
version = "v0.28.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:d8ebbd207f3d3266d4423ce4860c9f3794956306ded6c7ba312ecc69cdfbf04c"
|
||||||
|
name = "github.com/PuerkitoBio/purell"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:8098cd40cd09879efbf12e33bcd51ead4a66006ac802cd563a66c4f3373b9727"
|
||||||
|
name = "github.com/PuerkitoBio/urlesc"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd"
|
||||||
|
name = "github.com/beorn7/perks"
|
||||||
|
packages = ["quantile"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||||
|
name = "github.com/davecgh/go-spew"
|
||||||
|
packages = ["spew"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||||
|
version = "v1.1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:eb8f1b1913bffd6e788deee9fe4ba3a4d83267aff6045d3be33105e35ece290b"
|
||||||
|
name = "github.com/docker/spdystream"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"spdy",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "6480d4af844c189cf5dd913db24ddd339d3a4f85"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:3537d33c077a9666720dc987fddfecb07270606ac0a58f67abd08e3b252c0a45"
|
||||||
|
name = "github.com/emicklei/go-restful"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3eb9738c1697594ea6e71a7156a9bb32ed216cf0"
|
||||||
|
version = "v2.8.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:abfe129dc92b16fbf0cc9d6336096a2823151756f62072a700eb10754141b38e"
|
||||||
|
name = "github.com/ghodss/yaml"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:260f7ebefc63024c8dfe2c9f1a2935a89fa4213637a1f522f592f80c001cc441"
|
||||||
|
name = "github.com/go-openapi/jsonpointer"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3a0015ad55fa9873f41605d3e8f28cd279c32ab2"
|
||||||
|
version = "0.16.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:98abd61947ff5c7c6fcfec5473d02a4821ed3a2dd99a4fbfdb7925b0dd745546"
|
||||||
|
name = "github.com/go-openapi/jsonreference"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3fb327e6747da3043567ee86abd02bb6376b6be2"
|
||||||
|
version = "0.16.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:dfab391de021809e0041f0ab5648da6b74dd16a685472a1b8c3dc06b3dca1ee2"
|
||||||
|
name = "github.com/go-openapi/spec"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "384415f06ee238aae1df5caad877de6ceac3a5c4"
|
||||||
|
version = "0.16.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:44d7c281784896656e8c802c13a2e38d0f440904bdf6bdf968bb29f032df6a36"
|
||||||
|
name = "github.com/go-openapi/swag"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "becd2f08beafcca035645a8a101e0e3e18140458"
|
||||||
|
version = "0.16.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:a6afc27b2a73a5506832f3c5a1c19a30772cb69e7bd1ced4639eb36a55db224f"
|
||||||
|
name = "github.com/gogo/protobuf"
|
||||||
|
packages = [
|
||||||
|
"proto",
|
||||||
|
"sortkeys",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "c0656edd0d9eab7c66d1eb0c568f9039345796f7"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:78b8040ece2ff622580def2708b9eb0b2857711b6744c475439bf337e9c677ea"
|
||||||
|
name = "github.com/golang/glog"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "44145f04b68cf362d9c4df2182967c2275eaefed"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:63ccdfbd20f7ccd2399d0647a7d100b122f79c13bb83da9660b1598396fd9f62"
|
||||||
|
name = "github.com/golang/protobuf"
|
||||||
|
packages = [
|
||||||
|
"proto",
|
||||||
|
"ptypes",
|
||||||
|
"ptypes/any",
|
||||||
|
"ptypes/duration",
|
||||||
|
"ptypes/timestamp",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||||
|
version = "v1.2.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:f9425215dccf1c63f659ec781ca46bc81804341821d0cd8d2459c5b58f8bd067"
|
||||||
|
name = "github.com/google/gofuzz"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:27b4ab41ffdc76ad6db56db327a4db234a59588ef059fc3fd678ba0bc6b9094f"
|
||||||
|
name = "github.com/googleapis/gnostic"
|
||||||
|
packages = [
|
||||||
|
"OpenAPIv2",
|
||||||
|
"compiler",
|
||||||
|
"extensions",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "0c5108395e2debce0d731cf0287ddf7242066aba"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:b42cde0e1f3c816dd57f57f7bbcf05ca40263ad96f168714c130c611fc0856a6"
|
||||||
|
name = "github.com/hashicorp/golang-lru"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"simplelru",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768"
|
||||||
|
version = "v0.5.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:b7f860847a1d71f925ba9385ed95f1ebc0abfeb418a78e219ab61f48fdfeffad"
|
||||||
|
name = "github.com/howeyc/gopass"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:9a52adf44086cead3b384e5d0dbf7a1c1cce65e67552ee3383a8561c42a18cd3"
|
||||||
|
name = "github.com/imdario/mergo"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4"
|
||||||
|
version = "v0.3.6"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:8e36686e8b139f8fe240c1d5cf3a145bc675c22ff8e707857cdd3ae17b00d728"
|
||||||
|
name = "github.com/json-iterator/go"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "1624edc4454b8682399def8740d46db5e4362ba4"
|
||||||
|
version = "v1.1.5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:84a5a2b67486d5d67060ac393aa255d05d24ed5ee41daecd5635ec22657b6492"
|
||||||
|
name = "github.com/mailru/easyjson"
|
||||||
|
packages = [
|
||||||
|
"buffer",
|
||||||
|
"jlexer",
|
||||||
|
"jwriter",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "60711f1a8329503b04e1c88535f419d0bb440bff"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
|
||||||
|
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||||
|
packages = ["pbutil"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||||
|
version = "v1.0.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:2f42fa12d6911c7b7659738758631bec870b7e9b4c6be5444f963cdcfccc191f"
|
||||||
|
name = "github.com/modern-go/concurrent"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:c6aca19413b13dc59c220ad7430329e2ec454cc310bc6d8de2c7e2b93c18a0f6"
|
||||||
|
name = "github.com/modern-go/reflect2"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
|
||||||
|
version = "1.0.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:560e33aa696565dcb1b17786b74fdca712de4a74fe95b96d3ade9d84fc5ad883"
|
||||||
|
name = "github.com/openshift/api"
|
||||||
|
packages = [
|
||||||
|
"oauth/v1",
|
||||||
|
"route/v1",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "fac417a1916188eb39f7c36e279c832ce1c965df"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:8809516dcbfea012372b937388ec89bc929b97824840f43a3cd5c168fea1076e"
|
||||||
|
name = "github.com/operator-framework/operator-sdk"
|
||||||
|
packages = [
|
||||||
|
"pkg/k8sclient",
|
||||||
|
"pkg/sdk",
|
||||||
|
"pkg/sdk/internal/metrics",
|
||||||
|
"pkg/util/k8sutil",
|
||||||
|
"version",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "cb89692cdd145f084798749c16baaf44a6e62cac"
|
||||||
|
version = "v0.0.6"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:03bca087b180bf24c4f9060775f137775550a0834e18f0bca0520a868679dbd7"
|
||||||
|
name = "github.com/prometheus/client_golang"
|
||||||
|
packages = [
|
||||||
|
"prometheus",
|
||||||
|
"prometheus/promhttp",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||||
|
version = "v0.8.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
|
||||||
|
name = "github.com/prometheus/client_model"
|
||||||
|
packages = ["go"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:fad5a35eea6a1a33d6c8f949fbc146f24275ca809ece854248187683f52cc30b"
|
||||||
|
name = "github.com/prometheus/common"
|
||||||
|
packages = [
|
||||||
|
"expfmt",
|
||||||
|
"internal/bitbucket.org/ww/goautoneg",
|
||||||
|
"model",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "c7de2306084e37d54b8be01f3541a8464345e9a5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:26a2f5e891cc4d2321f18a0caa84c8e788663c17bed6a487f3cbe2c4295292d0"
|
||||||
|
name = "github.com/prometheus/procfs"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"internal/util",
|
||||||
|
"nfs",
|
||||||
|
"xfs",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "418d78d0b9a7b7de3a6bbc8a23def624cc977bb2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:44c7344434890241dde2d0d70d2e47355a1e012549e28ccf06f925aa6f8271ff"
|
||||||
|
name = "github.com/sirupsen/logrus"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "ba1b36c82c5e05c4f912a88eab0dcd91a171688f"
|
||||||
|
version = "v0.11.5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:9d8420bbf131d1618bde6530af37c3799340d3762cc47210c1d9532a4c3a2779"
|
||||||
|
name = "github.com/spf13/pflag"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
|
||||||
|
version = "v1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8"
|
||||||
|
name = "golang.org/x/crypto"
|
||||||
|
packages = ["ssh/terminal"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "0e37d006457bf46f9e6692014ba72ef82c33022c"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:1400b8e87c2c9bd486ea1a13155f59f8f02d385761206df05c0b7db007a53b2c"
|
||||||
|
name = "golang.org/x/net"
|
||||||
|
packages = [
|
||||||
|
"context",
|
||||||
|
"context/ctxhttp",
|
||||||
|
"http/httpguts",
|
||||||
|
"http2",
|
||||||
|
"http2/hpack",
|
||||||
|
"idna",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "2f5d2388922f370f4355f327fcf4cfe9f5583908"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:bc2b221d465bb28ce46e8d472ecdc424b9a9b541bd61d8c311c5f29c8dd75b1b"
|
||||||
|
name = "golang.org/x/oauth2"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"google",
|
||||||
|
"internal",
|
||||||
|
"jws",
|
||||||
|
"jwt",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "d2e6202438beef2727060aa7cabdd924d92ebfd9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:d468ba3543e2daeb578caa65430a5ed88bd3cce35700e7e84d1b377c03b1f230"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = [
|
||||||
|
"unix",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "90868a75fefd03942536221d7c0e2f84ec62a668"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:e33513a825fcd765e97b5de639a2f7547542d1a8245df0cef18e1fd390b778a9"
|
||||||
|
name = "golang.org/x/text"
|
||||||
|
packages = [
|
||||||
|
"collate",
|
||||||
|
"collate/build",
|
||||||
|
"internal/colltab",
|
||||||
|
"internal/gen",
|
||||||
|
"internal/tag",
|
||||||
|
"internal/triegen",
|
||||||
|
"internal/ucd",
|
||||||
|
"language",
|
||||||
|
"secure/bidirule",
|
||||||
|
"transform",
|
||||||
|
"unicode/bidi",
|
||||||
|
"unicode/cldr",
|
||||||
|
"unicode/norm",
|
||||||
|
"unicode/rangetable",
|
||||||
|
"width",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||||
|
version = "v0.3.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4"
|
||||||
|
name = "golang.org/x/time"
|
||||||
|
packages = ["rate"]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "fbb02b2291d28baffd63558aa44b4b56f178d650"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:45751dc3302c90ea55913674261b2d74286b05cdd8e3ae9606e02e4e77f4353f"
|
||||||
|
name = "golang.org/x/tools"
|
||||||
|
packages = [
|
||||||
|
"go/ast/astutil",
|
||||||
|
"imports",
|
||||||
|
"internal/fastwalk",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "e93be7f42f9fcac2c537bcd78bab08e6e63ed5bf"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:e2da54c7866453ac5831c61c7ec5d887f39328cac088c806553303bff4048e6f"
|
||||||
|
name = "google.golang.org/appengine"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"internal",
|
||||||
|
"internal/app_identity",
|
||||||
|
"internal/base",
|
||||||
|
"internal/datastore",
|
||||||
|
"internal/log",
|
||||||
|
"internal/modules",
|
||||||
|
"internal/remote_api",
|
||||||
|
"internal/urlfetch",
|
||||||
|
"urlfetch",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06"
|
||||||
|
version = "v1.2.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:ef72505cf098abdd34efeea032103377bec06abb61d8a06f002d5d296a4b1185"
|
||||||
|
name = "gopkg.in/inf.v0"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||||
|
version = "v2.2.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:802a62015ee56440c78b7a2e1e981f19bcccf105de730d6c8cc8db927581632a"
|
||||||
|
name = "k8s.io/api"
|
||||||
|
packages = [
|
||||||
|
"admissionregistration/v1alpha1",
|
||||||
|
"admissionregistration/v1beta1",
|
||||||
|
"apps/v1",
|
||||||
|
"apps/v1beta1",
|
||||||
|
"apps/v1beta2",
|
||||||
|
"authentication/v1",
|
||||||
|
"authentication/v1beta1",
|
||||||
|
"authorization/v1",
|
||||||
|
"authorization/v1beta1",
|
||||||
|
"autoscaling/v1",
|
||||||
|
"autoscaling/v2beta1",
|
||||||
|
"batch/v1",
|
||||||
|
"batch/v1beta1",
|
||||||
|
"batch/v2alpha1",
|
||||||
|
"certificates/v1beta1",
|
||||||
|
"core/v1",
|
||||||
|
"events/v1beta1",
|
||||||
|
"extensions/v1beta1",
|
||||||
|
"networking/v1",
|
||||||
|
"policy/v1beta1",
|
||||||
|
"rbac/v1",
|
||||||
|
"rbac/v1alpha1",
|
||||||
|
"rbac/v1beta1",
|
||||||
|
"scheduling/v1alpha1",
|
||||||
|
"settings/v1alpha1",
|
||||||
|
"storage/v1",
|
||||||
|
"storage/v1alpha1",
|
||||||
|
"storage/v1beta1",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "0f11257a8a25954878633ebdc9841c67d8f83bdb"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:3d7060fe0aa8467e57951f690e6bffad0d6088688c1dbcc234e0900ef8731431"
|
||||||
|
name = "k8s.io/apimachinery"
|
||||||
|
packages = [
|
||||||
|
"pkg/api/errors",
|
||||||
|
"pkg/api/meta",
|
||||||
|
"pkg/api/resource",
|
||||||
|
"pkg/apis/meta/internalversion",
|
||||||
|
"pkg/apis/meta/v1",
|
||||||
|
"pkg/apis/meta/v1/unstructured",
|
||||||
|
"pkg/apis/meta/v1beta1",
|
||||||
|
"pkg/conversion",
|
||||||
|
"pkg/conversion/queryparams",
|
||||||
|
"pkg/fields",
|
||||||
|
"pkg/labels",
|
||||||
|
"pkg/runtime",
|
||||||
|
"pkg/runtime/schema",
|
||||||
|
"pkg/runtime/serializer",
|
||||||
|
"pkg/runtime/serializer/json",
|
||||||
|
"pkg/runtime/serializer/protobuf",
|
||||||
|
"pkg/runtime/serializer/recognizer",
|
||||||
|
"pkg/runtime/serializer/streaming",
|
||||||
|
"pkg/runtime/serializer/versioning",
|
||||||
|
"pkg/selection",
|
||||||
|
"pkg/types",
|
||||||
|
"pkg/util/cache",
|
||||||
|
"pkg/util/clock",
|
||||||
|
"pkg/util/diff",
|
||||||
|
"pkg/util/errors",
|
||||||
|
"pkg/util/framer",
|
||||||
|
"pkg/util/httpstream",
|
||||||
|
"pkg/util/httpstream/spdy",
|
||||||
|
"pkg/util/intstr",
|
||||||
|
"pkg/util/json",
|
||||||
|
"pkg/util/net",
|
||||||
|
"pkg/util/remotecommand",
|
||||||
|
"pkg/util/runtime",
|
||||||
|
"pkg/util/sets",
|
||||||
|
"pkg/util/validation",
|
||||||
|
"pkg/util/validation/field",
|
||||||
|
"pkg/util/wait",
|
||||||
|
"pkg/util/yaml",
|
||||||
|
"pkg/version",
|
||||||
|
"pkg/watch",
|
||||||
|
"third_party/forked/golang/netutil",
|
||||||
|
"third_party/forked/golang/reflect",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "e386b2658ed20923da8cc9250e552f082899a1ee"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:cb947e649bf3de6e4a299e709d27de97b31fd7d2637e6ad9c1e2f3ededcadb13"
|
||||||
|
name = "k8s.io/client-go"
|
||||||
|
packages = [
|
||||||
|
"discovery",
|
||||||
|
"discovery/cached",
|
||||||
|
"discovery/fake",
|
||||||
|
"dynamic",
|
||||||
|
"kubernetes",
|
||||||
|
"kubernetes/fake",
|
||||||
|
"kubernetes/scheme",
|
||||||
|
"kubernetes/typed/admissionregistration/v1alpha1",
|
||||||
|
"kubernetes/typed/admissionregistration/v1alpha1/fake",
|
||||||
|
"kubernetes/typed/admissionregistration/v1beta1",
|
||||||
|
"kubernetes/typed/admissionregistration/v1beta1/fake",
|
||||||
|
"kubernetes/typed/apps/v1",
|
||||||
|
"kubernetes/typed/apps/v1/fake",
|
||||||
|
"kubernetes/typed/apps/v1beta1",
|
||||||
|
"kubernetes/typed/apps/v1beta1/fake",
|
||||||
|
"kubernetes/typed/apps/v1beta2",
|
||||||
|
"kubernetes/typed/apps/v1beta2/fake",
|
||||||
|
"kubernetes/typed/authentication/v1",
|
||||||
|
"kubernetes/typed/authentication/v1/fake",
|
||||||
|
"kubernetes/typed/authentication/v1beta1",
|
||||||
|
"kubernetes/typed/authentication/v1beta1/fake",
|
||||||
|
"kubernetes/typed/authorization/v1",
|
||||||
|
"kubernetes/typed/authorization/v1/fake",
|
||||||
|
"kubernetes/typed/authorization/v1beta1",
|
||||||
|
"kubernetes/typed/authorization/v1beta1/fake",
|
||||||
|
"kubernetes/typed/autoscaling/v1",
|
||||||
|
"kubernetes/typed/autoscaling/v1/fake",
|
||||||
|
"kubernetes/typed/autoscaling/v2beta1",
|
||||||
|
"kubernetes/typed/autoscaling/v2beta1/fake",
|
||||||
|
"kubernetes/typed/batch/v1",
|
||||||
|
"kubernetes/typed/batch/v1/fake",
|
||||||
|
"kubernetes/typed/batch/v1beta1",
|
||||||
|
"kubernetes/typed/batch/v1beta1/fake",
|
||||||
|
"kubernetes/typed/batch/v2alpha1",
|
||||||
|
"kubernetes/typed/batch/v2alpha1/fake",
|
||||||
|
"kubernetes/typed/certificates/v1beta1",
|
||||||
|
"kubernetes/typed/certificates/v1beta1/fake",
|
||||||
|
"kubernetes/typed/core/v1",
|
||||||
|
"kubernetes/typed/core/v1/fake",
|
||||||
|
"kubernetes/typed/events/v1beta1",
|
||||||
|
"kubernetes/typed/events/v1beta1/fake",
|
||||||
|
"kubernetes/typed/extensions/v1beta1",
|
||||||
|
"kubernetes/typed/extensions/v1beta1/fake",
|
||||||
|
"kubernetes/typed/networking/v1",
|
||||||
|
"kubernetes/typed/networking/v1/fake",
|
||||||
|
"kubernetes/typed/policy/v1beta1",
|
||||||
|
"kubernetes/typed/policy/v1beta1/fake",
|
||||||
|
"kubernetes/typed/rbac/v1",
|
||||||
|
"kubernetes/typed/rbac/v1/fake",
|
||||||
|
"kubernetes/typed/rbac/v1alpha1",
|
||||||
|
"kubernetes/typed/rbac/v1alpha1/fake",
|
||||||
|
"kubernetes/typed/rbac/v1beta1",
|
||||||
|
"kubernetes/typed/rbac/v1beta1/fake",
|
||||||
|
"kubernetes/typed/scheduling/v1alpha1",
|
||||||
|
"kubernetes/typed/scheduling/v1alpha1/fake",
|
||||||
|
"kubernetes/typed/settings/v1alpha1",
|
||||||
|
"kubernetes/typed/settings/v1alpha1/fake",
|
||||||
|
"kubernetes/typed/storage/v1",
|
||||||
|
"kubernetes/typed/storage/v1/fake",
|
||||||
|
"kubernetes/typed/storage/v1alpha1",
|
||||||
|
"kubernetes/typed/storage/v1alpha1/fake",
|
||||||
|
"kubernetes/typed/storage/v1beta1",
|
||||||
|
"kubernetes/typed/storage/v1beta1/fake",
|
||||||
|
"pkg/apis/clientauthentication",
|
||||||
|
"pkg/apis/clientauthentication/v1alpha1",
|
||||||
|
"pkg/version",
|
||||||
|
"plugin/pkg/client/auth/exec",
|
||||||
|
"plugin/pkg/client/auth/gcp",
|
||||||
|
"rest",
|
||||||
|
"rest/watch",
|
||||||
|
"testing",
|
||||||
|
"third_party/forked/golang/template",
|
||||||
|
"tools/auth",
|
||||||
|
"tools/cache",
|
||||||
|
"tools/clientcmd",
|
||||||
|
"tools/clientcmd/api",
|
||||||
|
"tools/clientcmd/api/latest",
|
||||||
|
"tools/clientcmd/api/v1",
|
||||||
|
"tools/metrics",
|
||||||
|
"tools/pager",
|
||||||
|
"tools/reference",
|
||||||
|
"tools/remotecommand",
|
||||||
|
"transport",
|
||||||
|
"transport/spdy",
|
||||||
|
"util/buffer",
|
||||||
|
"util/cert",
|
||||||
|
"util/exec",
|
||||||
|
"util/flowcontrol",
|
||||||
|
"util/homedir",
|
||||||
|
"util/integer",
|
||||||
|
"util/jsonpath",
|
||||||
|
"util/retry",
|
||||||
|
"util/workqueue",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "745ca830039794f7b927b8a2c2a58dcc1e8a0a72"
|
||||||
|
version = "kubernetes-1.10.7"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:b6d40560bfc8ef1300dbec6fae898810b59f26b1fd62d16692ab9befe8694dfc"
|
||||||
|
name = "k8s.io/code-generator"
|
||||||
|
packages = [
|
||||||
|
"cmd/client-gen",
|
||||||
|
"cmd/client-gen/args",
|
||||||
|
"cmd/client-gen/generators",
|
||||||
|
"cmd/client-gen/generators/fake",
|
||||||
|
"cmd/client-gen/generators/scheme",
|
||||||
|
"cmd/client-gen/generators/util",
|
||||||
|
"cmd/client-gen/path",
|
||||||
|
"cmd/client-gen/types",
|
||||||
|
"cmd/conversion-gen",
|
||||||
|
"cmd/conversion-gen/args",
|
||||||
|
"cmd/conversion-gen/generators",
|
||||||
|
"cmd/deepcopy-gen",
|
||||||
|
"cmd/deepcopy-gen/args",
|
||||||
|
"cmd/defaulter-gen",
|
||||||
|
"cmd/defaulter-gen/args",
|
||||||
|
"cmd/informer-gen",
|
||||||
|
"cmd/informer-gen/args",
|
||||||
|
"cmd/informer-gen/generators",
|
||||||
|
"cmd/lister-gen",
|
||||||
|
"cmd/lister-gen/args",
|
||||||
|
"cmd/lister-gen/generators",
|
||||||
|
"cmd/openapi-gen",
|
||||||
|
"cmd/openapi-gen/args",
|
||||||
|
"pkg/util",
|
||||||
|
]
|
||||||
|
pruneopts = "T"
|
||||||
|
revision = "7ead8f38b01cf8653249f5af80ce7b2c8aba12e2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:5249c83f0fb9e277b2d28c19eca814feac7ef05dc762e4deaf0a2e4b1a7c5df3"
|
||||||
|
name = "k8s.io/gengo"
|
||||||
|
packages = [
|
||||||
|
"args",
|
||||||
|
"examples/deepcopy-gen/generators",
|
||||||
|
"examples/defaulter-gen/generators",
|
||||||
|
"examples/set-gen/sets",
|
||||||
|
"generator",
|
||||||
|
"namer",
|
||||||
|
"parser",
|
||||||
|
"types",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "4242d8e6c5dba56827bb7bcf14ad11cda38f3991"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:396d31417183c4e572d907cee379238ea343c23dde9e028465ca3692498017c1"
|
||||||
|
name = "k8s.io/kube-openapi"
|
||||||
|
packages = [
|
||||||
|
"cmd/openapi-gen/args",
|
||||||
|
"pkg/common",
|
||||||
|
"pkg/generators",
|
||||||
|
"pkg/generators/rules",
|
||||||
|
"pkg/util/sets",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "e3762e86a74c878ffed47484592986685639c2cd"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
input-imports = [
|
||||||
|
"github.com/openshift/api/oauth/v1",
|
||||||
|
"github.com/openshift/api/route/v1",
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/k8sclient",
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk",
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/util/k8sutil",
|
||||||
|
"github.com/operator-framework/operator-sdk/version",
|
||||||
|
"github.com/sirupsen/logrus",
|
||||||
|
"k8s.io/api/apps/v1",
|
||||||
|
"k8s.io/api/batch/v1",
|
||||||
|
"k8s.io/api/core/v1",
|
||||||
|
"k8s.io/api/extensions/v1beta1",
|
||||||
|
"k8s.io/api/rbac/v1",
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors",
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource",
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||||
|
"k8s.io/apimachinery/pkg/fields",
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr",
|
||||||
|
"k8s.io/apimachinery/pkg/watch",
|
||||||
|
"k8s.io/client-go/kubernetes",
|
||||||
|
"k8s.io/client-go/kubernetes/fake",
|
||||||
|
"k8s.io/client-go/kubernetes/scheme",
|
||||||
|
"k8s.io/client-go/plugin/pkg/client/auth/gcp",
|
||||||
|
"k8s.io/client-go/rest",
|
||||||
|
"k8s.io/client-go/tools/remotecommand",
|
||||||
|
"k8s.io/code-generator/cmd/client-gen",
|
||||||
|
"k8s.io/code-generator/cmd/conversion-gen",
|
||||||
|
"k8s.io/code-generator/cmd/deepcopy-gen",
|
||||||
|
"k8s.io/code-generator/cmd/defaulter-gen",
|
||||||
|
"k8s.io/code-generator/cmd/informer-gen",
|
||||||
|
"k8s.io/code-generator/cmd/lister-gen",
|
||||||
|
"k8s.io/code-generator/cmd/openapi-gen",
|
||||||
|
"k8s.io/gengo/args",
|
||||||
|
]
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
|
||||||
|
# Force dep to vendor the code generators, which aren't imported just used at dev time.
|
||||||
|
# Picking a subpackage with Go code won't be necessary once https://github.com/golang/dep/pull/1545 is merged.
|
||||||
|
required = [
|
||||||
|
"k8s.io/code-generator/cmd/defaulter-gen",
|
||||||
|
"k8s.io/code-generator/cmd/deepcopy-gen",
|
||||||
|
"k8s.io/code-generator/cmd/conversion-gen",
|
||||||
|
"k8s.io/code-generator/cmd/client-gen",
|
||||||
|
"k8s.io/code-generator/cmd/lister-gen",
|
||||||
|
"k8s.io/code-generator/cmd/informer-gen",
|
||||||
|
"k8s.io/code-generator/cmd/openapi-gen",
|
||||||
|
"k8s.io/gengo/args",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "k8s.io/code-generator"
|
||||||
|
# revision for tag "kubernetes-1.10.1"
|
||||||
|
revision = "7ead8f38b01cf8653249f5af80ce7b2c8aba12e2"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "k8s.io/api"
|
||||||
|
revision = "0f11257a8a25954878633ebdc9841c67d8f83bdb"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "k8s.io/apiextensions-apiserver"
|
||||||
|
# revision for tag "kubernetes-1.10.1"
|
||||||
|
revision = "4347b330d0ff094db860f2f75fa725b4f4b53618"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "k8s.io/apimachinery"
|
||||||
|
# revision for tag "kubernetes-1.10.1"
|
||||||
|
revision = "e386b2658ed20923da8cc9250e552f082899a1ee"
|
||||||
|
# revision = "302974c03f7e50f16561ba237db776ab93594ef6"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "k8s.io/client-go"
|
||||||
|
version = "kubernetes-1.10.7"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "sigs.k8s.io/controller-runtime"
|
||||||
|
revision = "60bb251ad86f9b313653618aad0c2c53f41a6625"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/sirupsen/logrus"
|
||||||
|
version = "0.11.5"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/spf13/pflag"
|
||||||
|
version = "1.0.1"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/gogo/protobuf"
|
||||||
|
revision = "c0656edd0d9eab7c66d1eb0c568f9039345796f7"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/json-iterator/go"
|
||||||
|
version = "1.1.3"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/golang/glog"
|
||||||
|
revision = "44145f04b68cf362d9c4df2182967c2275eaefed"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/google/gofuzz"
|
||||||
|
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/googleapis/gnostic"
|
||||||
|
revision = "0c5108395e2debce0d731cf0287ddf7242066aba"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/modern-go/concurrent"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/modern-go/reflect2"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "gopkg.in/inf.v0"
|
||||||
|
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "gopkg.in/yaml.v2"
|
||||||
|
version = "2.2.1"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/PuerkitoBio/urlesc"
|
||||||
|
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/mailru/easyjson"
|
||||||
|
revision = "60711f1a8329503b04e1c88535f419d0bb440bff"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/ghodss/yaml"
|
||||||
|
revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
|
||||||
|
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
non-go = true
|
||||||
|
unused-packages = true
|
||||||
|
|
||||||
|
[[prune.project]]
|
||||||
|
name = "k8s.io/code-generator"
|
||||||
|
non-go = false
|
||||||
|
unused-packages = false
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/operator-framework/operator-sdk"
|
||||||
|
version = "=v0.0.6"
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Che/Codeready Operator
|
||||||
|
|
||||||
|
Che Operator creates Eclipse Che k8s and OpenShift resources such as pvcs, services, deployments, routes, ingresses etc.
|
||||||
|
|
||||||
|
The operator is a k8s a pod that runs an image with Go runtime and a compiled binary of an operator itself.
|
||||||
|
|
||||||
|
Though operator-sdk framework is used, Che operator is rather an installer since no CRD and API group are created,
|
||||||
|
and thus, the operator does not watch resources. Once deployment is completed, the operator pod exits.
|
||||||
|
|
||||||
|
## Pre-Reqs
|
||||||
|
|
||||||
|
OpenShift/K8S cluster with at least 4GB or RAM and 2 PVs, local `oc` or `kubectl`.
|
||||||
|
|
||||||
|
## How to deploy
|
||||||
|
|
||||||
|
Deploy script will create a namespace, operator service account and a rolebinding for it (admin privileges within the namespace),
|
||||||
|
and run an operator pod that will create all required objects and perform provisioning:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deploy/deploy.sh $infra $namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy to MiniKube to a default namespace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deploy/deploy.sh minikube
|
||||||
|
|
||||||
|
```
|
||||||
|
`minikube` as infra argument will instruct the script to detect MiniKube IP and pass global ingress domain value to the operator.
|
||||||
|
|
||||||
|
Create default namespace eclipse-che and deploy to k8s:
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/deploy.sh k8s
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a default eclipse-che namespace and deploy to OpenShift:
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a namespace of choice and deploy to OpenShift:
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/deploy.sh openshift myproject
|
||||||
|
```
|
||||||
|
|
||||||
|
If a namespace cannot be created (for example, it exists), a new namespace with a generated name (`eclipse-che${random int}`) will be created.
|
||||||
|
|
||||||
|
This will deploy Che operator with the default settings:
|
||||||
|
|
||||||
|
* upstream Che
|
||||||
|
* no tls
|
||||||
|
* no login with OpenShift in Che
|
||||||
|
* Postgres passwords are auto-generated, Keycloak admin password is `admin`
|
||||||
|
* Some object names are default ones (eg databases, users etc)
|
||||||
|
* Common PVC strategy (all workspaces use one shared PVC)
|
||||||
|
* All workspace objects get created in a target namespace (Che server uses service account token)
|
||||||
|
* Multi-host ingress strategy when on k8s
|
||||||
|
|
||||||
|
## Deploy custom image
|
||||||
|
|
||||||
|
Provide your own image in `deploy/config.yaml`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
CHE_IMAGE: "myRegistry/myImage:myTag"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy CodeReady Workspaces
|
||||||
|
|
||||||
|
In `deploy/config.yaml`:
|
||||||
|
|
||||||
|
```
|
||||||
|
CHE_FLAVOR: "codeready"
|
||||||
|
```
|
||||||
|
Che flavor is a string that will be used in names of certain objects like routes, realms, clients etc.
|
||||||
|
|
||||||
|
## Defaults and Configuration
|
||||||
|
|
||||||
|
To deploy to OpenShift with all defaults, no user input is required. You may configure Che installation in a configmap `deploy/config.yaml`.
|
||||||
|
The operator will use envs from this configmap and make decisions accordingly.
|
||||||
|
|
||||||
|
Currently, only the most critical envs are added to configmap, and it will expand in time, making it possible to fine tune Che before deploying
|
||||||
|
|
||||||
|
## What is deployed?
|
||||||
|
|
||||||
|
The Operator creates a handful of objects for:
|
||||||
|
|
||||||
|
* Postgres DB
|
||||||
|
* Keycloak/Red Hat SSO
|
||||||
|
* Che server itself
|
||||||
|
|
||||||
|
After Postgres and Keycloak pods start and health checks confirm the services are up, the operator run execs into db and keycloak pods to provision
|
||||||
|
databases, users, Keycloak realm, client and user. The operator watches respective deployments to get events signalling that the deployment is scaled to 1.
|
||||||
|
|
||||||
|
## How to configure installation
|
||||||
|
|
||||||
|
`deploy/config.yaml` is a config map with env variables that influences choices an operator makes. Each env is commented, and each one has defaults.
|
||||||
|
This configmap is an operator's env, and evn variables are then taken to Che server configmap.
|
||||||
|
|
||||||
|
What can be configured:
|
||||||
|
|
||||||
|
* external DB and Keycloak: `CHE_EXTERNAL_DB` and `CHE_EXTERNAL_KEYCLOAK` default to false.
|
||||||
|
If you do not need instances of Postgres and Keycloak and want to connect to own infra, set both envs to `true` and provide connection details in envs below the above booleans.
|
||||||
|
|
||||||
|
Your DB user **MUST** be a `SUPERUSER`.
|
||||||
|
|
||||||
|
**Important!** The operator does not perform Postgres and Keycloak provisioning if external instances are used.
|
||||||
|
Thus, you need to pre-create db and user for Che.
|
||||||
|
Also create (or use existing) realm and client that should be public and should have:
|
||||||
|
|
||||||
|
Redirect URIs: `${PROTOCOL}://${CHE_HOST}/*`
|
||||||
|
WebOrigins: `${PROTOCOL}://${CHE_HOST}`
|
||||||
|
|
||||||
|
* Login with OpenShift in Che. Not supported on k8s infra
|
||||||
|
|
||||||
|
* TLS. Set `TLS_SUPPORT` to true if you want to deploy Che in https mode.
|
||||||
|
When on k8s, make sure you create a secret in che namespace and provide its name in `TLS_SECRET_NAME`
|
||||||
|
|
||||||
|
* TLS and self signed certs. Provide a base64 string of your self signed cert in `SELF_SIGNED_CERT`. When on k8s, you can get it from crt part of the secret.
|
||||||
|
|
||||||
|
* Fake dns. If, for example, you want to deploy on k8s with a fake ingress domain of example.com and you need to point it to your Minikube IP, `HOST_ALIAS_IP` and `HOST_ALIAS_HOSTNAME` are the two envs to use.
|
||||||
|
|
||||||
|
## How to build and deploy own operator image
|
||||||
|
|
||||||
|
### Build an image
|
||||||
|
|
||||||
|
To build an Operator and package it into a Docker image run:
|
||||||
|
|
||||||
|
`docker build -t che-operator .`
|
||||||
|
|
||||||
|
If you want to deploy your custom Operator image to a remote k8s/OpenShift cluster, make sure you push an image and change its name in
|
||||||
|
`--image` and `--overrides` in kubectl/oc run command in the script.
|
||||||
|
|
||||||
|
### Build and deploy to a local cluster
|
||||||
|
|
||||||
|
Run `deploy/build_deploy_local.sh $infra $namespace`
|
||||||
|
|
||||||
|
Minishift and Minikube users will need to execute the below command to use VM Docker daemon when building an image:
|
||||||
|
|
||||||
|
```
|
||||||
|
eval $(minikube docker-env)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to add new envs to configmap?
|
||||||
|
|
||||||
|
If you need to add new envs to be added to Che configmap by default:
|
||||||
|
|
||||||
|
* add key:value to deploy/config.yaml
|
||||||
|
* add operator variable to `pkg/operator/vars.go` that will take its value from environment
|
||||||
|
* add env and operator variable as value to `pkg/operator/che_cm.go`
|
||||||
|
|
||||||
|
## Deploy Script
|
||||||
|
|
||||||
|
It is something quick and dirty and is likely to be substituted with a feature rich CLI.
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/operator"
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
oauth "github.com/openshift/api/oauth/v1"
|
||||||
|
route "github.com/openshift/api/route/v1"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
|
||||||
|
sdkVersion "github.com/operator-framework/operator-sdk/version"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
|
rbac "k8s.io/api/rbac/v1"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logrus.Info("Operator is running on: ", strings.ToUpper(util.GetInfra()))
|
||||||
|
k8sutil.AddToSDKScheme(appsv1.AddToScheme)
|
||||||
|
k8sutil.AddToSDKScheme(rbac.AddToScheme)
|
||||||
|
k8sutil.AddToSDKScheme(route.AddToScheme)
|
||||||
|
k8sutil.AddToSDKScheme(oauth.AddToScheme)
|
||||||
|
k8sutil.AddToSDKScheme(batchv1.AddToScheme)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersion() {
|
||||||
|
logrus.Infof("Go Version: %s", runtime.Version())
|
||||||
|
logrus.Infof("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
logrus.Infof("operator-sdk Version: %v", sdkVersion.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
printVersion()
|
||||||
|
operator.ReconcileChe()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
# This program and the accompanying materials are made
|
||||||
|
# available under the terms of the Eclipse Public License 2.0
|
||||||
|
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: EPL-2.0
|
||||||
|
#
|
||||||
|
# Contributors:
|
||||||
|
# Red Hat, Inc. - initial API and implementation
|
||||||
|
#set -e
|
||||||
|
BASE_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||||
|
if [[ $1 = "k8s" || $1 = "minikube" ]]; then
|
||||||
|
CMD="kubectl"
|
||||||
|
CREATE_PROJECT="create namespace"
|
||||||
|
else
|
||||||
|
CREATE_PROJECT="new-project"
|
||||||
|
CMD="oc"
|
||||||
|
OPENSHIFT_API_URL="$(oc whoami --show-server)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $1 = "minikube" ]]; then
|
||||||
|
echo "Using MiniKube VM Docker daemon"
|
||||||
|
eval $(minikube docker-env)
|
||||||
|
MINIKUBE_IP="$(minikube ip)"
|
||||||
|
if [[ -z "${MINIKUBE_IP}" ]]; then
|
||||||
|
echo "Failed to get MiniKube IP. Make sure MiniKube is running. Current status:"
|
||||||
|
minikube status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
INGRESS_DOMAIN="${MINIKUBE_IP}.nip.io"
|
||||||
|
echo "Using MiniKube ingress domain: "${INGRESS_DOMAIN}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building Che Operator..."
|
||||||
|
docker build -t che-operator ${BASE_DIR}/../
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "Failed to build an Operator"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$2" ]]; then
|
||||||
|
NAMESPACE="eclipse-che"
|
||||||
|
else
|
||||||
|
NAMESPACE=$2
|
||||||
|
fi
|
||||||
|
|
||||||
|
${CMD} ${CREATE_PROJECT} ${NAMESPACE}
|
||||||
|
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "Namespace ${NAMESPACE} cannot be crated. Generating namespace name"
|
||||||
|
NAMESPACE="eclipse-che$(( ( RANDOM % 10 ) + 1 ))$(( ( RANDOM % 10 ) + 1 ))"
|
||||||
|
${CMD} ${CREATE_PROJECT} ${NAMESPACE}
|
||||||
|
fi
|
||||||
|
|
||||||
|
${CMD} create serviceaccount che-operator -n=${NAMESPACE}
|
||||||
|
${CMD} create rolebinding che-operator --clusterrole=admin --serviceaccount=${NAMESPACE}:che-operator -n=${NAMESPACE}
|
||||||
|
${CMD} create -f ${BASE_DIR}/config.yaml -n=${NAMESPACE}
|
||||||
|
${CMD} patch cm/che-operator -p "{\"data\": {\"CHE_INFRA_KUBERNETES_INGRESS_DOMAIN\": \"${INGRESS_DOMAIN}\", \"CHE_OPENSHIFT_API_URL\": \"${OPENSHIFT_API_URL}\"}}" -n ${NAMESPACE}
|
||||||
|
${CMD} delete pod che-operator -n=${NAMESPACE} 2> /dev/null || true
|
||||||
|
${CMD} run -ti "che-operator" \
|
||||||
|
--restart='Never' \
|
||||||
|
--serviceaccount='che-operator' \
|
||||||
|
--image='che-operator' \
|
||||||
|
--env="OPERATOR_NAME=che-operator" \
|
||||||
|
--overrides='{"spec":{"containers":[{"image": "che-operator", "name": "che-operator", "imagePullPolicy":"IfNotPresent","envFrom":[{"configMapRef":{"name":"che-operator"}}]}]}}' \
|
||||||
|
-n=${NAMESPACE}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
# This program and the accompanying materials are made
|
||||||
|
# available under the terms of the Eclipse Public License 2.0
|
||||||
|
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: EPL-2.0
|
||||||
|
#
|
||||||
|
# Contributors:
|
||||||
|
# Red Hat, Inc. - initial API and implementation
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: che-operator
|
||||||
|
data:
|
||||||
|
############################
|
||||||
|
## General Che Settings ####
|
||||||
|
############################
|
||||||
|
|
||||||
|
# CHE flavor. Upstream `che` or Red Hat `codeready`. Defaults to `che`
|
||||||
|
CHE_FLAVOR: ""
|
||||||
|
# Docker image for Che server. Defaults to eclipse/che-server:latest. Keep blank unless you need to deploy your custom image
|
||||||
|
CHE_IMAGE: ""
|
||||||
|
# TLS support in Che. Defaults to false
|
||||||
|
CHE_TLS_SUPPORT: ""
|
||||||
|
# PVC strategy for Che workspaces. Defaults to 'common' where all workspaces use one shared PVC
|
||||||
|
# A 'unique' strategy implies that each workspaces gets own PVCs
|
||||||
|
CHE_INFRA_KUBERNETES_PVC_STRATEGY: ""
|
||||||
|
# Workspace PVC claim. Defaults to 1Gi. It is recommended to increase it when using shared PVC
|
||||||
|
CHE_INFRA_KUBERNETES_PVC_QUANTITY: ""
|
||||||
|
# Content of your self signed cert as string, e.g:
|
||||||
|
# cat root ca.crt | base64 -w 0
|
||||||
|
CHE_SELF__SIGNED__CERT: ""
|
||||||
|
# enable Login with OpenShift in Codeready Workspaces. OpenShift only. Defaults to false
|
||||||
|
CHE_OPENSHIFT_OAUTH: ""
|
||||||
|
# OpenShift API endpoint URL. Required only when OPENSHIFT_OAUTH is true. Auto detected
|
||||||
|
CHE_OPENSHIFT_API_URL: ""
|
||||||
|
# Plugin registry URL. Defaults to https://che-plugin-registry.openshift.io
|
||||||
|
CHE_WORKSPACE_PLUGIN__REGISTRY__URL: ""
|
||||||
|
# Ask for password update at first login as Che admin user. Defaults to true, i.e. you will be asked to update password
|
||||||
|
CHE_UPDATE_CHE_ADMIN_PASSWORD: ""
|
||||||
|
|
||||||
|
############################
|
||||||
|
## Che Proxy settings ##
|
||||||
|
############################
|
||||||
|
|
||||||
|
# proxy settings for workspace master, for example:
|
||||||
|
# -Dhttp.proxyHost=host -Dhttp.proxyPort=8080 -Dhttps.proxyHost=host -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts='localhost^|127.0.0.1^|*.foo.com'
|
||||||
|
CHE_WORKSPACE_MASTER_PROXY_JAVA_OPTS: ""
|
||||||
|
# proxy settings for workspace JVM, Maven and workspace agent:
|
||||||
|
# -Dhttp.proxyHost=host -Dhttp.proxyPort=8080 -Dhttps.proxyHost=host -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts='localhost^|127.0.0.1^|*.foo.com'
|
||||||
|
CHE_WORKSPACE_PROXY_JAVA_OPTS: ""
|
||||||
|
# http proxy for workspaces, e.g. 'http://myproxy:8051'
|
||||||
|
CHE_WORKSPACE_HTTP__PROXY: ""
|
||||||
|
# https proxy for workspaces, e.g. 'http://myproxy:8051'
|
||||||
|
CHE_WORKSPACE_HTTPS__PROXY: ""
|
||||||
|
# no_proxy for workspaces, e.g. 'localhost,10.2.34.54'
|
||||||
|
CHE_WORKSPACE_NO__PROXY: ""
|
||||||
|
|
||||||
|
############################
|
||||||
|
## Database settings ##
|
||||||
|
############################
|
||||||
|
|
||||||
|
# use external existing Postgres DB. Defaults to false which means a new instance of Postgres will be started
|
||||||
|
# when set to true, provide connection details and make sure DB user is a SUPERUSER
|
||||||
|
CHE_EXTERNAL_DB: ""
|
||||||
|
# DB hostname
|
||||||
|
CHE_DB_HOSTNAME: ""
|
||||||
|
# DB port
|
||||||
|
CHE_DB_PORT: "5432"
|
||||||
|
# DB database
|
||||||
|
CHE_DB_DATABASE: ""
|
||||||
|
# DB username
|
||||||
|
CHE_JDBC_USERNAME: ""
|
||||||
|
# DB password
|
||||||
|
CHE_JDBC_PASSWORD: ""
|
||||||
|
# Your DB admin password
|
||||||
|
CHE_DB_ADMIN_PASSWORD: ""
|
||||||
|
|
||||||
|
############################
|
||||||
|
## Keycloak settings #######
|
||||||
|
############################
|
||||||
|
|
||||||
|
# Connect to an existing external Keycloak
|
||||||
|
# and skip deploying a dedicated Keycloak instance. Defaults to false
|
||||||
|
CHE_EXTERNAL_KEYCLOAK: ""
|
||||||
|
# Keycloak/Red Hat SSO
|
||||||
|
CHE_KEYCLOAK_AUTH__SERVER__URL: ""
|
||||||
|
# Keyloak Admin Username.
|
||||||
|
# Not required when connecting to an existing realm
|
||||||
|
CHE_KEYCLOAK_ADMIN_USERNAME: ""
|
||||||
|
# Keycloak admin password. Auto-generated if empty
|
||||||
|
CHE_KEYCLOAK_ADMIN_PASSWORD: "admin"
|
||||||
|
# Red Hat SSO realm
|
||||||
|
CHE_KEYCLOAK_REALM: ""
|
||||||
|
# Red Hat SSO client ID
|
||||||
|
CHE_KEYCLOAK_CLIENT__ID: ""
|
||||||
|
|
||||||
|
############################
|
||||||
|
## k8s only settings #######
|
||||||
|
############################
|
||||||
|
|
||||||
|
# k8s ingress domain
|
||||||
|
CHE_INFRA_KUBERNETES_INGRESS_DOMAIN: ""
|
||||||
|
# ingress strategy. Defaults to multi-host
|
||||||
|
CHE_INFRA_KUBERNETES_SERVER__STRATEGY: ""
|
||||||
|
# defaults to nginx
|
||||||
|
CHE_INGRESS_CLASS: ""
|
||||||
|
CHE_INFRA_KUBERNETES_TLS__SECRET: ""
|
||||||
|
|
||||||
|
############################
|
||||||
|
## Operator settings #######
|
||||||
|
############################
|
||||||
|
|
||||||
|
# wait timeout for an Operator to watch deployments
|
||||||
|
# defaults to 420 seconds
|
||||||
|
WAIT_DEPLOYMENT_TIMEOUT: ""
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
# This program and the accompanying materials are made
|
||||||
|
# available under the terms of the Eclipse Public License 2.0
|
||||||
|
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: EPL-2.0
|
||||||
|
#
|
||||||
|
# Contributors:
|
||||||
|
# Red Hat, Inc. - initial API and implementation
|
||||||
|
#set -e
|
||||||
|
BASE_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||||
|
if [[ $1 = "k8s" || $1 = "minikube" || $1 = "docker4mac" ]]; then
|
||||||
|
CMD="kubectl"
|
||||||
|
CREATE_PROJECT="create namespace"
|
||||||
|
else
|
||||||
|
CREATE_PROJECT="new-project"
|
||||||
|
CMD="oc"
|
||||||
|
OPENSHIFT_API_URL="$(oc whoami --show-server)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $1 = "minikube" ]]; then
|
||||||
|
MINIKUBE_IP="$(minikube ip)"
|
||||||
|
if [[ -z "${MINIKUBE_IP}" ]]; then
|
||||||
|
echo "Failed to get MiniKube IP. Make sure MiniKube is running. Current status:"
|
||||||
|
minikube status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
INGRESS_DOMAIN="${MINIKUBE_IP}.nip.io"
|
||||||
|
echo "Using MiniKube ingress domain: "${INGRESS_DOMAIN}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $1 = "docker4mac" ]]; then
|
||||||
|
K8S_IP="$(${CMD} get services --namespace ingress-nginx -o jsonpath='{.items[*].spec.clusterIP}')"
|
||||||
|
if [[ -z "${K8S_IP}" ]]; then
|
||||||
|
echo "Failed to get Ingress Cluster IP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
INGRESS_DOMAIN="${K8S_IP}.nip.io"
|
||||||
|
echo "Using Docker4Mac ingress domain: ${INGRESS_DOMAIN}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$2" ]]; then
|
||||||
|
NAMESPACE="eclipse-che"
|
||||||
|
else
|
||||||
|
NAMESPACE=$2
|
||||||
|
fi
|
||||||
|
|
||||||
|
${CMD} ${CREATE_PROJECT} ${NAMESPACE}
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo "Namespace ${NAMESPACE} cannot be crated. Generating namespace name"
|
||||||
|
NAMESPACE="eclipse-che$(( ( RANDOM % 10 ) + 1 ))$(( ( RANDOM % 10 ) + 1 ))"
|
||||||
|
${CMD} ${CREATE_PROJECT} ${NAMESPACE}
|
||||||
|
fi
|
||||||
|
${CMD} create serviceaccount che-operator -n=${NAMESPACE}
|
||||||
|
${CMD} create rolebinding che-operator --clusterrole=admin --serviceaccount=${NAMESPACE}:che-operator -n=${NAMESPACE}
|
||||||
|
|
||||||
|
${CMD} create -f ${BASE_DIR}/config.yaml -n=${NAMESPACE}
|
||||||
|
${CMD} patch cm/che-operator -p "{\"data\": {\"CHE_INFRA_KUBERNETES_INGRESS_DOMAIN\": \"${INGRESS_DOMAIN}\", \"CHE_OPENSHIFT_API_URL\": \"${OPENSHIFT_API_URL}\"}}" -n ${NAMESPACE}
|
||||||
|
${CMD} delete pod che-operator -n=${NAMESPACE} 2> /dev/null || true
|
||||||
|
${CMD} run -ti "che-operator" \
|
||||||
|
--restart='Never' \
|
||||||
|
--serviceaccount='che-operator' \
|
||||||
|
--image='eivantsov/che-operator' \
|
||||||
|
--overrides='{"spec":{"containers":[{"image": "eivantsov/che-operator", "name": "che-operator", "imagePullPolicy":"IfNotPresent","envFrom":[{"configMapRef":{"name":"che-operator"}}]}]}}' \
|
||||||
|
-n=${NAMESPACE}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/opt/eap/bin/kcadm.sh config credentials --server http://0.0.0.0:8080/auth \
|
||||||
|
--realm master \
|
||||||
|
--user $keycloakAdminUserName \
|
||||||
|
--password $keycloakAdminPassword \
|
||||||
|
$trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh update realms/master -s sslRequired=none $trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh create realms -s realm=$keycloakRealm \
|
||||||
|
-s enabled=true \
|
||||||
|
-s sslRequired=none \
|
||||||
|
-s registrationAllowed=true \
|
||||||
|
-s loginTheme=$keycloakTheme \
|
||||||
|
-s accountTheme=$keycloakTheme \
|
||||||
|
-s adminTheme=$keycloakTheme \
|
||||||
|
-s emailTheme=$keycloakTheme \
|
||||||
|
$trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh create clients -r $keycloakRealm \
|
||||||
|
-s clientId=$keycloakClientId \
|
||||||
|
-s 'webOrigins=["$protocol://$cheHost"]' \
|
||||||
|
-s 'redirectUris=["$protocol://$cheHost/*"]' \
|
||||||
|
-s 'directAccessGrantsEnabled'=true \
|
||||||
|
-s publicClient=true $trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh create users -s username=admin \
|
||||||
|
-s email=\"admin@admin.com\" \
|
||||||
|
-s enabled=true -r $keycloakRealm \
|
||||||
|
-s 'requiredActions=[$requiredActions]' \
|
||||||
|
$trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh set-password -r $keycloakRealm --username admin \
|
||||||
|
--new-password admin \
|
||||||
|
$trustStoreCommandArg \
|
||||||
|
&& /opt/eap/bin/kcadm.sh add-roles -r $keycloakRealm \
|
||||||
|
--uusername admin \
|
||||||
|
--cclientid broker \
|
||||||
|
--rolename read-token \
|
||||||
|
$trustStoreCommandArg
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Merge two maps: general Che config and k8s only
|
||||||
|
func addMap(a map[string]string, b map[string]string) {
|
||||||
|
for k, v := range b {
|
||||||
|
a[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCheConfigMap(cheHost string, keycloakURL string) *corev1.ConfigMap {
|
||||||
|
infra := util.GetInfra()
|
||||||
|
cheLabels := map[string]string{"app": "che"}
|
||||||
|
openshiftOAuth := util.GetEnvBool("CHE_OPENSHIFT_OAUTH", false)
|
||||||
|
ingressDomain := util.GetEnv("CHE_INFRA_KUBERNETES_INGRESS_DOMAIN", "")
|
||||||
|
strategy := util.GetEnv("CHE_INFRA_KUBERNETES_SERVER__STRATEGY", "multi-host")
|
||||||
|
workspacesNamespace := namespace
|
||||||
|
tls := "false"
|
||||||
|
openShiftIdentityProviderId := "NULL"
|
||||||
|
if openshiftOAuth {
|
||||||
|
workspacesNamespace = ""
|
||||||
|
openShiftIdentityProviderId = "openshift-v3"
|
||||||
|
}
|
||||||
|
if tlsSupport {
|
||||||
|
protocol = "https"
|
||||||
|
wsprotocol = "wss"
|
||||||
|
tls = "true"
|
||||||
|
}
|
||||||
|
// general envs
|
||||||
|
cheEnv := map[string]string{
|
||||||
|
"CHE_MULTIUSER": "true",
|
||||||
|
"CHE_HOST": cheHost,
|
||||||
|
"CHE_PORT": "8080",
|
||||||
|
"CHE_API": protocol + "://" + cheHost + "/api",
|
||||||
|
"CHE_WEBSOCKET_ENDPOINT": wsprotocol + "://" + cheHost + "/api/websocket",
|
||||||
|
// todo Make configurable
|
||||||
|
"CHE_DEBUG_SERVER": "false",
|
||||||
|
"CHE_INFRASTRUCTURE_ACTIVE": infra,
|
||||||
|
"CHE_INFRA_KUBERNETES_BOOTSTRAPPER_BINARY__URL": protocol + "://" + cheHost + "/agent-binaries/linux_amd64/bootstrapper/bootstrapper",
|
||||||
|
"CHE_INFRA_OPENSHIFT_PROJECT": workspacesNamespace,
|
||||||
|
"CHE_INFRA_KUBERNETES_PVC_STRATEGY": pvcStrategy,
|
||||||
|
"CHE_INFRA_KUBERNETES_PVC_QUANTITY": pvcClaimSize,
|
||||||
|
"CHE_INFRA_OPENSHIFT_TLS__ENABLED": tls,
|
||||||
|
"CHE_INFRA_KUBERNETES_TRUST__CERTS": tls,
|
||||||
|
"CHE_JDBC_URL": "jdbc:postgresql://" + postgresHostName + ":" + postgresPort + "/" + chePostgresDb,
|
||||||
|
"CHE_JDBC_USERNAME": chePostgresUser,
|
||||||
|
// todo Create a secret for it?
|
||||||
|
"CHE_JDBC_PASSWORD": chePostgresPassword,
|
||||||
|
"CHE_LOG_LEVEL": "INFO",
|
||||||
|
"CHE_KEYCLOAK_AUTH__SERVER__URL": keycloakURL + "/auth",
|
||||||
|
"CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER": openShiftIdentityProviderId,
|
||||||
|
"CHE_PREDEFINED_STACKS_RELOAD__ON__START": "true",
|
||||||
|
"CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME": "che-workspace",
|
||||||
|
"JAVA_OPTS": "-XX:MaxRAMFraction=2 -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 " +
|
||||||
|
"-XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 " +
|
||||||
|
"-XX:AdaptiveSizePolicyWeight=90 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap " +
|
||||||
|
"-Dsun.zip.disableMemoryMapping=true -Xms20m " + cheWsmasterProxyJavaOptions,
|
||||||
|
"CHE_WORKSPACE_AUTO_START": "true",
|
||||||
|
"CHE_KEYCLOAK_REALM": keycloakRealm,
|
||||||
|
"CHE_KEYCLOAK_CLIENT__ID": keycloakClientId,
|
||||||
|
"CHE_INFRA_KUBERNETES_WORKSPACE__UNRECOVERABLE__EVENTS": "FailedMount,FailedScheduling,MountVolume.SetUp failed,Failed to pull image",
|
||||||
|
"CHE_WORKSPACE_AGENT_DEV_INACTIVE__STOP__TIMEOUT__MS": "-1",
|
||||||
|
"CHE_WORKSPACE_JAVA_OPTIONS": "-XX:MaxRAM=150m -XX:MaxRAMFraction=2 -XX:+UseParallelGC " +
|
||||||
|
"-XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 " +
|
||||||
|
"-Dsun.zip.disableMemoryMapping=true " +
|
||||||
|
"-Xms20m -Djava.security.egd=file:/dev/./urandom " + cheWorkspaceProxyJavaOptions,
|
||||||
|
"CHE_WORKSPACE_MAVEN__OPTIONS": "-XX:MaxRAM=150m -XX:MaxRAMFraction=2 -XX:+UseParallelGC " +
|
||||||
|
"-XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 " +
|
||||||
|
"-Dsun.zip.disableMemoryMapping=true " +
|
||||||
|
"-Xms20m -Djava.security.egd=file:/dev/./urandom " + cheWorkspaceProxyJavaOptions,
|
||||||
|
"CHE_WORKSPACE_HTTP__PROXY__JAVA__OPTIONS": cheWorkspaceProxyJavaOptions,
|
||||||
|
"CHE_WORKSPACE_HTTP__PROXY": cheWorkspaceHttpProxy,
|
||||||
|
"CHE_WORKSPACE_HTTPS__PROXY": cheWorkspaceHttpsProxy,
|
||||||
|
"CHE_WORKSPACE_NO__PROXY": cheWorkspaceNoProxy,
|
||||||
|
"CHE_WORKSPACE_PLUGIN__REGISTRY__URL": pluginRegistryUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
// k8s specific envs
|
||||||
|
k8sCheEnv := map[string]string{
|
||||||
|
"CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_FS__GROUP": "0",
|
||||||
|
"CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_RUN__AS__USER": "0",
|
||||||
|
"CHE_INFRA_KUBERNETES_NAMESPACE": workspacesNamespace,
|
||||||
|
"CHE_INFRA_KUBERNETES_INGRESS_DOMAIN": ingressDomain,
|
||||||
|
"CHE_INFRA_KUBERNETES_SERVER__STRATEGY": strategy,
|
||||||
|
"CHE_INFRA_KUBERNETES_INGRESS_ANNOTATIONS__JSON": "{\"kubernetes.io/ingress.class\": " + ingressClass + ", \"nginx.ingress.kubernetes.io/rewrite-target\": \"/\",\"nginx.ingress.kubernetes.io/ssl-redirect\": " + tls + ",\"nginx.ingress.kubernetes.io/proxy-connect-timeout\": \"3600\",\"nginx.ingress.kubernetes.io/proxy-read-timeout\": \"3600\"}",
|
||||||
|
}
|
||||||
|
|
||||||
|
if infra == "kubernetes" {
|
||||||
|
addMap(cheEnv, k8sCheEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &corev1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "che",
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: cheLabels,
|
||||||
|
},
|
||||||
|
Data: cheEnv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCheConfigMap creates a ConfigMaps that holds all Che configuration
|
||||||
|
func CreateCheConfigMap(cheHost string, keycloakURL string) *corev1.ConfigMap {
|
||||||
|
cm := newCheConfigMap(cheHost, keycloakURL)
|
||||||
|
if err := sdk.Create(cm); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create Che ConfigMap : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
rbac "k8s.io/api/rbac/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRoleBinding(kind string,name string, serviceAccountName string, roleName string, roleKind string) *rbac.RoleBinding {
|
||||||
|
labels := map[string]string{"app": "che"}
|
||||||
|
return &rbac.RoleBinding{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: kind,
|
||||||
|
APIVersion: rbac.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Subjects: []rbac.Subject{
|
||||||
|
{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: serviceAccountName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleRef: rbac.RoleRef{
|
||||||
|
Name: roleName,
|
||||||
|
Kind: roleKind,
|
||||||
|
APIGroup: "rbac.authorization.k8s.io",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRoleBinding creates a role binding for che related service accounts that enables it to access/edit resources in a target (current) namespace
|
||||||
|
func CreateRoleBinding(kind string, name string, serviceAccountName string, roleName string, roleKind string) *rbac.RoleBinding {
|
||||||
|
rb := newRoleBinding(kind, name, serviceAccountName, roleName, roleKind)
|
||||||
|
err := sdk.Create(rb)
|
||||||
|
if err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create %s RoleBinding: %v", name, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rb
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newServiceAccount(name string) *corev1.ServiceAccount {
|
||||||
|
labels := map[string]string{"app": "che"}
|
||||||
|
return &corev1.ServiceAccount{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServiceAccount creates a service account that Che will use to creates workspace objects
|
||||||
|
// Che service account requires a RoleBinding
|
||||||
|
func CreateServiceAccount(name string) (*corev1.ServiceAccount) {
|
||||||
|
rt := newServiceAccount(name)
|
||||||
|
if err := sdk.Create(rt); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create service account : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rt
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newCheDeployment(cheImage string) *appsv1.Deployment {
|
||||||
|
cheLabels := map[string]string{"app": "che"}
|
||||||
|
optionalEnv := true
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Deployment",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "che",
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: cheLabels,
|
||||||
|
},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: cheLabels},
|
||||||
|
Strategy: appsv1.DeploymentStrategy{
|
||||||
|
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||||
|
},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: cheLabels,
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
ServiceAccountName: "che",
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "che",
|
||||||
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
|
Image: cheImage,
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{
|
||||||
|
Name: "http",
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "http-debug",
|
||||||
|
ContainerPort: 8000,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "jgroups-ping",
|
||||||
|
ContainerPort: 8888,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resources: corev1.ResourceRequirements{
|
||||||
|
Requests: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("512Mi"),
|
||||||
|
},
|
||||||
|
Limits: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("1Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
Handler: corev1.Handler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/api/system/state",
|
||||||
|
Port: intstr.IntOrString{
|
||||||
|
Type: intstr.Int,
|
||||||
|
IntVal: int32(8080),
|
||||||
|
},
|
||||||
|
Scheme: corev1.URISchemeHTTP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: 25,
|
||||||
|
FailureThreshold: 5,
|
||||||
|
TimeoutSeconds: 5,
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
Handler: corev1.Handler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "/api/system/state",
|
||||||
|
Port: intstr.IntOrString{
|
||||||
|
Type: intstr.Int,
|
||||||
|
IntVal: int32(8080),
|
||||||
|
},
|
||||||
|
Scheme: corev1.URISchemeHTTP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: 50,
|
||||||
|
FailureThreshold: 3,
|
||||||
|
TimeoutSeconds: 3,
|
||||||
|
},
|
||||||
|
EnvFrom: []corev1.EnvFromSource{
|
||||||
|
{
|
||||||
|
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{Name: "che"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "OPENSHIFT_KUBE_PING_NAMESPACE",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
FieldRef: &corev1.ObjectFieldSelector{
|
||||||
|
FieldPath: "metadata.namespace"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "CHE_SELF__SIGNED__CERT",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
Key: "ca.crt",
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: "self-signed-certificate",
|
||||||
|
},
|
||||||
|
Optional: &optionalEnv,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCheDeployment creates a deployment with che ConfigMap in env
|
||||||
|
func CreateCheDeployment(cheImage string) (*appsv1.Deployment, error) {
|
||||||
|
k8s := GetK8SConfig()
|
||||||
|
deployment := newCheDeployment(cheImage)
|
||||||
|
if err := sdk.Create(deployment); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create Che deployment : %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// wait until deployment is scaled to 1 replica to proceed with other deployments
|
||||||
|
k8s.GetDeploymentStatus(deployment)
|
||||||
|
logrus.Info("Che is available at: " + protocol + "://" + cheHost)
|
||||||
|
deploymentTime := time.Since(StartTime)
|
||||||
|
logrus.Info("Deployment took ", deploymentTime)
|
||||||
|
return deployment, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"io/ioutil"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
keycloakName = "keycloak"
|
||||||
|
keycloakImage = "registry.access.redhat.com/redhat-sso-7/sso72-openshift:1.2-8"
|
||||||
|
trustpass = util.GeneratePasswd(12)
|
||||||
|
addCertToTrustStoreCommand = "echo \"${CHE_SELF__SIGNED__CERT}\" > /opt/eap/bin/openshift.crt" +
|
||||||
|
" && keytool -importcert -alias HOSTDOMAIN" +
|
||||||
|
" -keystore /opt/eap/bin/openshift.jks" +
|
||||||
|
" -file /opt/eap/bin/openshift.crt -storepass " + trustpass + " -noprompt" +
|
||||||
|
" && keytool -importkeystore -srckeystore $JAVA_HOME/jre/lib/security/cacerts" +
|
||||||
|
" -destkeystore /opt/eap/bin/openshift.jks" +
|
||||||
|
" -srcstorepass changeit -deststorepass " + trustpass
|
||||||
|
|
||||||
|
trustStoreCommandArg = " --truststore /opt/eap/bin/openshift.jks --trustpass " + trustpass + " "
|
||||||
|
startCommand = "sed -i 's/WILDCARD/ANY/g' /opt/eap/bin/launch/keycloak-spi.sh && /opt/eap/bin/openshift-launch.sh -b 0.0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newKeycloakDeployment() *appsv1.Deployment {
|
||||||
|
optionalEnv := true
|
||||||
|
var command string
|
||||||
|
selfSignedCert := util.GetEnv("CHE_SELF__SIGNED__CERT", "")
|
||||||
|
ssoTrustStoreEnv := corev1.EnvVar{Name: "SSO_TRUSTSTORE", Value: "openshift.jks"}
|
||||||
|
ssoTrustStoreDir := corev1.EnvVar{Name: "SSO_TRUSTSTORE_DIR", Value: "/opt/eap/bin"}
|
||||||
|
ssoTrustStorePassword := corev1.EnvVar{Name: "SSO_TRUSTSTORE_PASSWORD", Value: trustpass,}
|
||||||
|
|
||||||
|
keycloakEnv := []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "PROXY_ADDRESS_FORWARDING",
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DB_SERVICE_PREFIX_MAPPING",
|
||||||
|
Value: "keycloak-postgresql=DB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "KEYCLOAK_POSTGRESQL_SERVICE_HOST",
|
||||||
|
Value: "postgres",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "KEYCLOAK_POSTGRESQL_SERVICE_PORT",
|
||||||
|
Value: "5432",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DB_DATABASE",
|
||||||
|
Value: keycloakName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DB_USERNAME",
|
||||||
|
Value: keycloakName,
|
||||||
|
},
|
||||||
|
// todo Create a secret for it?
|
||||||
|
{
|
||||||
|
Name: "DB_PASSWORD",
|
||||||
|
Value: keycloakPostgresPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "SSO_ADMIN_USERNAME",
|
||||||
|
Value: keycloakAdminUserName,
|
||||||
|
},
|
||||||
|
// todo Create a secret for it?
|
||||||
|
{
|
||||||
|
Name: "SSO_ADMIN_PASSWORD",
|
||||||
|
Value: keycloakAdminPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DB_VENDOR",
|
||||||
|
Value: "POSTGRES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "CHE_SELF__SIGNED__CERT",
|
||||||
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
SecretKeyRef: &corev1.SecretKeySelector{
|
||||||
|
Key: "ca.crt",
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: "self-signed-certificate",
|
||||||
|
},
|
||||||
|
Optional: &optionalEnv,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(selfSignedCert) > 0 {
|
||||||
|
command = addCertToTrustStoreCommand + " && " + startCommand
|
||||||
|
keycloakEnv = append(keycloakEnv, ssoTrustStoreDir, ssoTrustStorePassword, ssoTrustStoreEnv)
|
||||||
|
} else {
|
||||||
|
command = startCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Deployment",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: keycloakName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: keycloakLabels,
|
||||||
|
},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: keycloakLabels},
|
||||||
|
Strategy: appsv1.DeploymentStrategy{
|
||||||
|
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||||
|
},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: keycloakLabels,
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: keycloakName,
|
||||||
|
Image: keycloakImage,
|
||||||
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
|
Command: []string{
|
||||||
|
"/bin/sh",
|
||||||
|
},
|
||||||
|
Args: []string{
|
||||||
|
"-c",
|
||||||
|
command,
|
||||||
|
},
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{
|
||||||
|
Name: keycloakName,
|
||||||
|
ContainerPort: 8080,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resources: corev1.ResourceRequirements{
|
||||||
|
Requests: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("512Mi"),
|
||||||
|
},
|
||||||
|
Limits: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("2Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
Handler: corev1.Handler{
|
||||||
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
|
Path: "auth/js/keycloak.js",
|
||||||
|
Port: intstr.IntOrString{
|
||||||
|
Type: intstr.Int,
|
||||||
|
IntVal: int32(8080),
|
||||||
|
},
|
||||||
|
Scheme: corev1.URISchemeHTTP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: 25,
|
||||||
|
FailureThreshold: 10,
|
||||||
|
TimeoutSeconds: 5,
|
||||||
|
},
|
||||||
|
Env: keycloakEnv,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKeycloakDeployment creates a deployment that starts a container with Keycloak
|
||||||
|
func CreateKeycloakDeployment() (*appsv1.Deployment, error) {
|
||||||
|
k8s := GetK8SConfig()
|
||||||
|
deployment := newKeycloakDeployment()
|
||||||
|
if err := sdk.Create(deployment); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+keycloakName+" deployment : %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// wait until deployment is scaled to 1 replica to proceed with other deployments
|
||||||
|
k8s.GetDeploymentStatus(deployment)
|
||||||
|
return deployment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetKeycloakProvisionCommand(keycloakURL string, cheHost string) (command string) {
|
||||||
|
openShiftApiUrl := util.GetEnv("CHE_OPENSHIFT_API_URL", "")
|
||||||
|
requiredActions := ""
|
||||||
|
if updateAdminPassword {
|
||||||
|
requiredActions = "\"UPDATE_PASSWORD\""
|
||||||
|
}
|
||||||
|
file, err := ioutil.ReadFile("/tmp/keycloak_provision") //
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to find keycloak entrypoint file", err)
|
||||||
|
}
|
||||||
|
keycloakTheme := "keycloak"
|
||||||
|
if cheFlavor == "codeready" {
|
||||||
|
keycloakTheme = "rh-sso"
|
||||||
|
|
||||||
|
}
|
||||||
|
str := string(file)
|
||||||
|
r := strings.NewReplacer("$keycloakURL", keycloakURL,
|
||||||
|
"$keycloakAdminUserName", keycloakAdminUserName,
|
||||||
|
"$keycloakAdminPassword", keycloakAdminPassword,
|
||||||
|
"$keycloakRealm", keycloakRealm,
|
||||||
|
"$keycloakClientId", keycloakClientId,
|
||||||
|
"$keycloakTheme", keycloakTheme,
|
||||||
|
"$protocol", protocol,
|
||||||
|
"$cheHost", cheHost,
|
||||||
|
"$trustStoreCommandArg", trustStoreCommandArg,
|
||||||
|
"$requiredActions", requiredActions)
|
||||||
|
createRealmClientUserCommand := r.Replace(str)
|
||||||
|
|
||||||
|
createOpenShiftIdentityProviderCommand :=
|
||||||
|
"/opt/eap/bin/kcadm.sh create identity-provider/instances -r " + keycloakRealm +
|
||||||
|
" -s alias=openshift-v3 -s providerId=openshift-v3 -s enabled=true -s storeToken=true" +
|
||||||
|
" -s addReadTokenRoleOnCreate=true -s config.useJwksUrl=true" +
|
||||||
|
" -s config.clientId=" + oAuthClientName + " -s config.clientSecret=" + oauthSecret +
|
||||||
|
" -s config.baseUrl=" + openShiftApiUrl +
|
||||||
|
" -s config.defaultScope=user:full" + trustStoreCommandArg
|
||||||
|
|
||||||
|
command = createRealmClientUserCommand
|
||||||
|
if openshiftOAuth {
|
||||||
|
command = createRealmClientUserCommand + " && " + createOpenShiftIdentityProviderCommand
|
||||||
|
}
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPostgresDeployment() *appsv1.Deployment {
|
||||||
|
name := "postgres"
|
||||||
|
return &appsv1.Deployment{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Deployment",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "postgres",
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: postgresLabels,
|
||||||
|
},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: postgresLabels},
|
||||||
|
Strategy: appsv1.DeploymentStrategy{
|
||||||
|
Type: appsv1.DeploymentStrategyType("Recreate"),
|
||||||
|
},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: postgresLabels,
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Volumes: []corev1.Volume{
|
||||||
|
{
|
||||||
|
Name: name + "-data",
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||||
|
ClaimName: name + "-data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Image: "registry.access.redhat.com/rhscl/postgresql-96-rhel7:1-25",
|
||||||
|
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
ContainerPort: 5432,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Resources: corev1.ResourceRequirements{
|
||||||
|
Requests: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("512Mi"),
|
||||||
|
},
|
||||||
|
Limits: corev1.ResourceList{
|
||||||
|
corev1.ResourceMemory: resource.MustParse("1Gi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: name + "-data",
|
||||||
|
MountPath: "/var/lib/pgsql/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
Handler: corev1.Handler{
|
||||||
|
Exec: &corev1.ExecAction{
|
||||||
|
Command: []string{
|
||||||
|
"/bin/sh",
|
||||||
|
"-i",
|
||||||
|
"-c",
|
||||||
|
"psql -h 127.0.0.1 -U " + chePostgresUser + " -q -d " + chePostgresDb + " -c 'SELECT 1'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InitialDelaySeconds: 15,
|
||||||
|
FailureThreshold: 10,
|
||||||
|
SuccessThreshold: 1,
|
||||||
|
TimeoutSeconds: 5,
|
||||||
|
},
|
||||||
|
Env: []corev1.EnvVar{
|
||||||
|
{
|
||||||
|
Name: "POSTGRESQL_USER",
|
||||||
|
Value: chePostgresUser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "POSTGRESQL_PASSWORD",
|
||||||
|
Value: chePostgresPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "POSTGRESQL_DATABASE",
|
||||||
|
Value: chePostgresDb,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "POSTGRESQL_ADMIN_PASSWORD",
|
||||||
|
Value: postgresAdminPassword,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePostgresDeployment creates a deployment with 1 Postgres pod in spec. DB, user and password for Che are created
|
||||||
|
// via env variables while DB, user for Keycloak are provisioned in CreatePgJob
|
||||||
|
func CreatePostgresDeployment() *appsv1.Deployment {
|
||||||
|
k8s := GetK8SConfig()
|
||||||
|
deployment := newPostgresDeployment()
|
||||||
|
if err := sdk.Create(deployment); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create Postgres deployment : %v", err)
|
||||||
|
logrus.Error("Operator is exiting")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// wait until deployment is scaled to 1 replica to proceed with other deployments
|
||||||
|
k8s.GetDeploymentStatus(deployment)
|
||||||
|
return deployment
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pgCommand = "psql -c \"CREATE USER keycloak WITH PASSWORD '" + keycloakPostgresPassword + "'\" " +
|
||||||
|
"&& psql -c \"CREATE DATABASE keycloak\" " +
|
||||||
|
"&& psql -c \"GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak\" " +
|
||||||
|
"&& psql -c \"ALTER USER " + chePostgresUser + " WITH SUPERUSER\""
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExecIntoPod(podName string, provisionCommand string) {
|
||||||
|
k8s := GetK8SConfig()
|
||||||
|
command := []string{"/bin/bash", "-c", provisionCommand}
|
||||||
|
logrus.Infof("Provisioning resources in pod %s", podName)
|
||||||
|
// print std if operator is run in debug mode (TODO)
|
||||||
|
_, stderr, err := k8s.ExecToPod(command, podName, util.GetNamespace())
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error exec'ing into pod %v: , command: %s", err, command)
|
||||||
|
logrus.Fatalf(stderr)
|
||||||
|
}
|
||||||
|
logrus.Info("Provisioning completed")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/api/extensions/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newIngress(name string, serviceName string, port int) *v1beta1.Ingress {
|
||||||
|
labels := cheLabels
|
||||||
|
if name == "keycloak" {
|
||||||
|
labels = keycloakLabels
|
||||||
|
}
|
||||||
|
tls := "false"
|
||||||
|
if tlsSupport {
|
||||||
|
tls = "true"
|
||||||
|
}
|
||||||
|
host := ""
|
||||||
|
path := "/"
|
||||||
|
if name == "keycloak" && strategy != "multi-host" {
|
||||||
|
path = "/auth"
|
||||||
|
}
|
||||||
|
if strategy == "multi-host" {
|
||||||
|
host = name + "-" + namespace + "." + ingressDomain
|
||||||
|
} else if strategy == "single-host" {
|
||||||
|
host = ingressDomain
|
||||||
|
}
|
||||||
|
tlsIngress := []v1beta1.IngressTLS{}
|
||||||
|
if tlsSupport {
|
||||||
|
tlsIngress = []v1beta1.IngressTLS{
|
||||||
|
{
|
||||||
|
Hosts: []string{
|
||||||
|
ingressDomain,
|
||||||
|
},
|
||||||
|
SecretName: tlsSecretName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v1beta1.Ingress{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Ingress",
|
||||||
|
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"kubernetes.io/ingress.class": ingressClass,
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-read-timeout": "3600",
|
||||||
|
"nginx.ingress.kubernetes.io/proxy-connect-timeout": "3600",
|
||||||
|
"nginx.ingress.kubernetes.io/ssl-redirect": tls,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
Spec: v1beta1.IngressSpec{
|
||||||
|
TLS: tlsIngress,
|
||||||
|
Rules: []v1beta1.IngressRule{
|
||||||
|
{
|
||||||
|
Host: host,
|
||||||
|
IngressRuleValue: v1beta1.IngressRuleValue{
|
||||||
|
HTTP: &v1beta1.HTTPIngressRuleValue{
|
||||||
|
|
||||||
|
Paths: []v1beta1.HTTPIngressPath{
|
||||||
|
{
|
||||||
|
Backend: v1beta1.IngressBackend{
|
||||||
|
ServiceName: serviceName,
|
||||||
|
ServicePort: intstr.FromInt(port),
|
||||||
|
},
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIngress(name string, serviceName string, port int) *v1beta1.Ingress {
|
||||||
|
ingress := newIngress(name, serviceName, port)
|
||||||
|
if err := sdk.Create(ingress); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+name+" ingress : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ingress
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateIngressIfNotExists(name string, serviceName string, port int) *v1beta1.Ingress {
|
||||||
|
ingress := newIngress(name, serviceName, port)
|
||||||
|
err := sdk.Get(ingress)
|
||||||
|
if err != nil {
|
||||||
|
return createIngress(name, serviceName, port)
|
||||||
|
}
|
||||||
|
return ingress
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type k8s struct {
|
||||||
|
clientset kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetK8SConfig() *k8s {
|
||||||
|
config, err := rest.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
client := k8s{}
|
||||||
|
client.clientset, err = kubernetes.NewForConfig(config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &client
|
||||||
|
}
|
||||||
|
// GetDeploymentStatus listens to deployment events and checks replicas once MODIFIED event is received
|
||||||
|
func (o *k8s) GetDeploymentStatus(deployment *appsv1.Deployment) {
|
||||||
|
api := o.clientset.AppsV1()
|
||||||
|
timeoutStr := util.GetEnv("WAIT_DEPLOYMENT_TIMEOUT", "420")
|
||||||
|
timeout, err := strconv.ParseInt(timeoutStr, 10, 64)
|
||||||
|
listOptions := metav1.ListOptions{
|
||||||
|
FieldSelector: fields.OneTermEqualSelector("metadata.name", deployment.Name).String(),
|
||||||
|
TimeoutSeconds: &timeout,
|
||||||
|
}
|
||||||
|
_, err = api.Deployments(namespace).Get(deployment.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Failed to get %s deployment", deployment.Name)
|
||||||
|
}
|
||||||
|
watcher, err := api.Deployments(util.GetNamespace()).Watch(listOptions)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
ch := watcher.ResultChan()
|
||||||
|
logrus.Printf("Waiting for deployment %s. Default timeout: %v seconds", deployment.Name, timeout)
|
||||||
|
for event := range ch {
|
||||||
|
dc, ok := event.Object.(*appsv1.Deployment)
|
||||||
|
if !ok {
|
||||||
|
log.Fatal("Unexpected type")
|
||||||
|
}
|
||||||
|
|
||||||
|
//check before watching in case the deployment is already scaled to 1
|
||||||
|
err := sdk.Get(deployment)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Failed to get %s deployment: %s", deployment.Name, err)
|
||||||
|
}
|
||||||
|
if deployment.Status.AvailableReplicas == 1 {
|
||||||
|
logrus.Infof("%s successfully deployed", deployment.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event.Type {
|
||||||
|
case watch.Error:
|
||||||
|
watcher.Stop()
|
||||||
|
case watch.Modified:
|
||||||
|
if dc.Status.AvailableReplicas == 1 {
|
||||||
|
logrus.Infof("%s successfully deployed", deployment.Name)
|
||||||
|
watcher.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployment.Status.AvailableReplicas != 1 {
|
||||||
|
logrus.Errorf("Failed to verify a successful %s deployment. Operator is exiting", deployment.Name)
|
||||||
|
eventList := o.GetEvents(deployment.Name).Items
|
||||||
|
for i := range eventList {
|
||||||
|
logrus.Errorf("Event message: %v", eventList[i].Message)
|
||||||
|
}
|
||||||
|
deploymentPod := o.GetDeploymentPod(deployment.Name)
|
||||||
|
o.GetPodLogs(deploymentPod)
|
||||||
|
logrus.Errorf("Command to get deployment logs: kubectl logs deployment/%s -n=%s", deployment.Name, deployment.Namespace)
|
||||||
|
logrus.Fatalf("Get k8s events: kubectl get events "+
|
||||||
|
"--field-selector "+
|
||||||
|
"involvedObject.name=$(kubectl get pods -l=app=%s -n=%s"+
|
||||||
|
" -o=jsonpath='{.items[0].metadata.name}') -n=%s", deployment.Name, deployment.Namespace, deployment.Namespace)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GetEvents returns a list of events filtered by involvedObject
|
||||||
|
func (o *k8s) GetEvents(deploymentName string) (list *corev1.EventList) {
|
||||||
|
eventListOptions := metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("involvedObject.fieldPath", "spec.containers{"+deploymentName+"}").String()}
|
||||||
|
deploymentEvents, _ := o.clientset.CoreV1().Events(namespace).List(eventListOptions)
|
||||||
|
return deploymentEvents
|
||||||
|
}
|
||||||
|
// GetLogs prints stderr or stdout from a selected pod. Log size is capped at 60000 bytes
|
||||||
|
func (o *k8s) GetPodLogs(podName string) () {
|
||||||
|
var limitBytes int64 = 60000
|
||||||
|
req := o.clientset.CoreV1().Pods(namespace).GetLogs(podName, &corev1.PodLogOptions{LimitBytes: &limitBytes},
|
||||||
|
)
|
||||||
|
readCloser, err := req.Stream()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Pod error log: %v", err)
|
||||||
|
} else {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, err = io.Copy(buf, readCloser)
|
||||||
|
logrus.Infof("Pod log: %v", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ExecToPod executes a remote command in a selected pod
|
||||||
|
func (o *k8s) ExecToPod(command []string, podName, namespace string) (string, string, error) {
|
||||||
|
|
||||||
|
req := o.clientset.CoreV1().RESTClient().Post().
|
||||||
|
Resource("pods").
|
||||||
|
Name(podName).
|
||||||
|
Namespace(namespace).
|
||||||
|
SubResource("exec")
|
||||||
|
|
||||||
|
req.VersionedParams(&corev1.PodExecOptions{
|
||||||
|
Command: command,
|
||||||
|
Stdin: false,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
TTY: false,
|
||||||
|
}, scheme.ParameterCodec)
|
||||||
|
|
||||||
|
config, _ := rest.InClusterConfig()
|
||||||
|
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("error while creating executor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
var stdin io.Reader
|
||||||
|
err = exec.Stream(remotecommand.StreamOptions{
|
||||||
|
Stdin: stdin,
|
||||||
|
Stdout: &stdout,
|
||||||
|
Stderr: &stderr,
|
||||||
|
Tty: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return stdout.String(), stderr.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.String(), stderr.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeploymentPod queries all pod is a selected namespace by LabelSelector
|
||||||
|
func (o *k8s) GetDeploymentPod(name string) (podName string) {
|
||||||
|
api := o.clientset.CoreV1()
|
||||||
|
listOptions := metav1.ListOptions{
|
||||||
|
LabelSelector: "app=" + name,
|
||||||
|
}
|
||||||
|
podList, _ := api.Pods(namespace).List(listOptions)
|
||||||
|
podListItems := podList.Items
|
||||||
|
if len(podListItems) == 0 {
|
||||||
|
logrus.Error("Failed to find pod to exec into")
|
||||||
|
logrus.Errorf("Pod list: %v ", podList.Items)
|
||||||
|
}
|
||||||
|
// expecting only one pod to be there
|
||||||
|
podName = podListItems[0].Name
|
||||||
|
return podName
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fakeK8s = fakeClientSet()
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeClientSet() *k8s {
|
||||||
|
client := k8s{}
|
||||||
|
client.clientset = fake.NewSimpleClientset()
|
||||||
|
return &client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDeploymentPod(t *testing.T) {
|
||||||
|
|
||||||
|
// create a fake pod
|
||||||
|
_, err := fakeK8s.clientset.CoreV1().Pods("eclipse-che").Create(&corev1.Pod{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fake-pod",
|
||||||
|
Namespace: "eclipse-che",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"app": "postgres",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pod := fakeK8s.GetDeploymentPod("postgres")
|
||||||
|
if len(pod) == 0 {
|
||||||
|
logrus.Fatal("Test failed. No pods found by label")
|
||||||
|
}
|
||||||
|
logrus.Infof("Test passed. Pod %s found", pod)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEvents(t *testing.T) {
|
||||||
|
|
||||||
|
// fire up an event with fake-pod as involvedObject
|
||||||
|
message := "This is a fake event about a fake pod"
|
||||||
|
_, err := fakeK8s.clientset.CoreV1().Events("eclipse-che").Create(&corev1.Event{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Event",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "fake-event",
|
||||||
|
Namespace: "eclipse-che",
|
||||||
|
},
|
||||||
|
InvolvedObject: corev1.ObjectReference{
|
||||||
|
FieldPath: "spec.containers{fake-pod}",
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
Message: message,
|
||||||
|
Reason: "Testing event filtering",
|
||||||
|
Type: "Normal",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
events := fakeK8s.GetEvents("fake-pod")
|
||||||
|
// test if function returns any events
|
||||||
|
fakePodEvents := events.Items
|
||||||
|
if len(fakePodEvents) == 0 {
|
||||||
|
logrus.Fatal("Test failed No events found")
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Test passed. Found %v event", len(fakePodEvents))
|
||||||
|
}
|
||||||
|
// test if event message matches
|
||||||
|
fakePodEventMessage := events.Items[0].Message
|
||||||
|
if len(fakePodEventMessage) != len(message) {
|
||||||
|
logrus.Fatalf("Test failed. Message to be received: %s, but got %s ", message, fakePodEventMessage)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Test passed. Expected event message: %s. Received event message %s", message, fakePodEventMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCheDeployment(t *testing.T) {
|
||||||
|
|
||||||
|
// create a fake Che deployment
|
||||||
|
fakeImage := "fake-image"
|
||||||
|
deployment, err := fakeK8s.clientset.AppsV1().Deployments("eclipse-che").Create(newCheDeployment(fakeImage))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Deployment %s created", deployment.Name)
|
||||||
|
|
||||||
|
if deployment.Spec.Template.Spec.Containers[0].Image != fakeImage {
|
||||||
|
logrus.Fatalf("Expected %s, but got %s", fakeImage, deployment.Spec.Template.Spec.Containers[0].Image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCheConfigMap(t *testing.T) {
|
||||||
|
// set current infra to k8s
|
||||||
|
err := os.Setenv("INFRA", "kubernetes")
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set fake ingress domain env and check if configmap grabs its value
|
||||||
|
fakeDomain := "fake-domain"
|
||||||
|
fakeDomainEnv := "CHE_INFRA_KUBERNETES_INGRESS_DOMAIN"
|
||||||
|
err = os.Setenv(fakeDomainEnv, fakeDomain)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a configmap
|
||||||
|
cm, err := fakeK8s.clientset.CoreV1().ConfigMaps("eclipse-che").Create(newCheConfigMap(cheHost, keycloakURL))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
logrus.Infof("ConfigMap %s successfully created", cm.Name)
|
||||||
|
|
||||||
|
// check env and its value
|
||||||
|
cmEnv := cm.Data
|
||||||
|
if val, ok := cmEnv["CHE_INFRA_KUBERNETES_INGRESS_DOMAIN"]; ok {
|
||||||
|
logrus.Infof("Env found. CHE_INFRA_KUBERNETES_INGRESS_DOMAIN=%s", val)
|
||||||
|
if val != fakeDomain {
|
||||||
|
logrus.Fatalf("Expected %s but got %s", fakeDomain, val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logrus.Fatalf("Env %s not found", fakeDomain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPostgresDeployment(t *testing.T) {
|
||||||
|
|
||||||
|
// create a fake Postgres deployment
|
||||||
|
deployment, err := fakeK8s.clientset.AppsV1().Deployments("eclipse-che").Create(newPostgresDeployment())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Deployment %s created", deployment.Name)
|
||||||
|
password := deployment.Spec.Template.Spec.Containers[0].Env[1].Value
|
||||||
|
|
||||||
|
// check if password was properly generated and added to container env
|
||||||
|
if len(password) != 12 {
|
||||||
|
logrus.Fatalf("Expecting password lenght to be 12, but got %v", len(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewKeycloakDeployment(t *testing.T) {
|
||||||
|
|
||||||
|
//// create a fake Keycloak deployment
|
||||||
|
err := os.Setenv("CHE_SELF__SIGNED__CERT", "mycrt")
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
deployment, err := fakeK8s.clientset.AppsV1().Deployments("eclipse-che").Create(newKeycloakDeployment())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
logrus.Infof("Deployment %s created", deployment.Name)
|
||||||
|
|
||||||
|
// check if setting self-signed-certificate cert updates env for the container properly
|
||||||
|
selfSignedEnv := deployment.Spec.Template.Spec.Containers[0].Env
|
||||||
|
var foundEnv bool
|
||||||
|
ssoEnv := "SSO_TRUSTSTORE"
|
||||||
|
for i := range selfSignedEnv {
|
||||||
|
env := selfSignedEnv[i].Name
|
||||||
|
if env == ssoEnv {
|
||||||
|
logrus.Infof("%s env found: %s", ssoEnv, env)
|
||||||
|
foundEnv = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundEnv {
|
||||||
|
logrus.Fatalf("No %s env found", ssoEnv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
oauth "github.com/openshift/api/oauth/v1"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newOAuthClient(name string, oauthSecret string, keycloakURL string, keycloakRealm string ) *oauth.OAuthClient {
|
||||||
|
return &oauth.OAuthClient{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "OAuthClient",
|
||||||
|
APIVersion: oauth.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Labels: map[string]string{"app":"che"},
|
||||||
|
},
|
||||||
|
|
||||||
|
Secret: oauthSecret,
|
||||||
|
RedirectURIs: []string{
|
||||||
|
keycloakURL + "/auth/realms/" + keycloakRealm +"/broker/openshift-v3/endpoint",
|
||||||
|
},
|
||||||
|
GrantMethod: oauth.GrantHandlerPrompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateOAuthClient (name string, oauthSecret string, keycloakURL string, keycloakRealm string ) *oauth.OAuthClient {
|
||||||
|
oauthClient := newOAuthClient(name, oauthSecret, keycloakURL, keycloakRealm)
|
||||||
|
if err := sdk.Create(oauthClient); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create oAuth client : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return oauthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pvc(name string, pvcClaimSize string, labels map[string]string) *corev1.PersistentVolumeClaim {
|
||||||
|
|
||||||
|
return &corev1.PersistentVolumeClaim{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "PersistentVolumeClaim",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
|
AccessModes: []corev1.PersistentVolumeAccessMode{
|
||||||
|
// todo Make configurable
|
||||||
|
corev1.ReadWriteOnce,
|
||||||
|
},
|
||||||
|
Resources: corev1.ResourceRequirements{
|
||||||
|
Requests: corev1.ResourceList{
|
||||||
|
corev1.ResourceName(corev1.ResourceStorage): resource.MustParse(pvcClaimSize),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// CreatePVC creates a persistent volume claim with a given name, claim size, access mode and labels
|
||||||
|
func CreatePVC(name string, pvcClaimSize string, labels map[string]string) *corev1.PersistentVolumeClaim {
|
||||||
|
pvc := pvc(name, pvcClaimSize, labels)
|
||||||
|
if err := sdk.Create(pvc); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+name+" PVC : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pvc
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReconcileChe creates and watches k8s and OpenShift objects
|
||||||
|
// This is where the operator logic happens
|
||||||
|
var StartTime = time.Now()
|
||||||
|
|
||||||
|
func ReconcileChe() {
|
||||||
|
infra := util.GetInfra()
|
||||||
|
k8s := GetK8SConfig()
|
||||||
|
// service accounts, roles and role-binding for Che
|
||||||
|
CreateServiceAccount("che")
|
||||||
|
CreateServiceAccount("che-workspace")
|
||||||
|
CreateNewRole("exec", []string{"pods/exec"}, []string{"create"})
|
||||||
|
CreateNewRole("view", []string{"pods"}, []string{"list"})
|
||||||
|
CreateRoleBinding("RoleBinding", "che", "che", "edit", "ClusterRole")
|
||||||
|
CreateRoleBinding("RoleBinding", "che-workspace-exec", "che-workspace", "exec", "Role")
|
||||||
|
CreateRoleBinding("RoleBinding", "che-workspace-view", "che-workspace", "view", "Role")
|
||||||
|
|
||||||
|
if len(selfSignedCert) > 0 {
|
||||||
|
CreateCertSecret()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and watch Postgres related resources, unless external DB hostname is provided
|
||||||
|
if ! externalDb {
|
||||||
|
CreatePVC("postgres-data", "1Gi", postgresLabels)
|
||||||
|
CreateService("postgres", postgresLabels, "postgres", 5432)
|
||||||
|
CreatePostgresDeployment()
|
||||||
|
// provision 2 databases, 1 user, and grant superuser privileges to Che pg user
|
||||||
|
ExecIntoPod(k8s.GetDeploymentPod("postgres"), pgCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Che related envs required for Keycloak
|
||||||
|
CreateService("che-host", cheLabels, "http", 8080)
|
||||||
|
|
||||||
|
if tlsSupport {
|
||||||
|
protocol = "https"
|
||||||
|
wsprotocol = "wss"
|
||||||
|
}
|
||||||
|
// create Che route when on OpenShift infra
|
||||||
|
if infra == "openshift" {
|
||||||
|
rt, _ := CreateRouteIfNotExists(cheFlavor, "che-host")
|
||||||
|
cheHost = rt.Spec.Host
|
||||||
|
} else {
|
||||||
|
// create Che ingress when on k8s infra
|
||||||
|
CreateIngressIfNotExists(cheFlavor, "che-host", 8080)
|
||||||
|
cheHost = ingressDomain
|
||||||
|
if strategy == "multi-host" {
|
||||||
|
cheHost = cheFlavor + "-" + namespace + "." + ingressDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and watch Keycloak resources
|
||||||
|
if ! externalKeycloak {
|
||||||
|
CreateService("keycloak", keycloakLabels, "keycloak", 8080)
|
||||||
|
|
||||||
|
if infra == "openshift" {
|
||||||
|
keycloakRoute, _ := CreateRouteIfNotExists("keycloak", "keycloak")
|
||||||
|
keycloakURL = protocol + "://" + keycloakRoute.Spec.Host
|
||||||
|
} else {
|
||||||
|
CreateIngressIfNotExists("keycloak", "keycloak", 8080)
|
||||||
|
keycloakURL = protocol + "://" + ingressDomain
|
||||||
|
if strategy == "multi-host" {
|
||||||
|
keycloakURL = protocol + "://" + "keycloak-" + namespace + "." + ingressDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keycloakDc, _ := CreateKeycloakDeployment()
|
||||||
|
err := sdk.Update(keycloakDc)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to update Keycloak deployment : %v", err)
|
||||||
|
}
|
||||||
|
// provision Keycloak realm, client and user
|
||||||
|
ExecIntoPod(k8s.GetDeploymentPod("keycloak"), GetKeycloakProvisionCommand(keycloakURL, cheHost))
|
||||||
|
}
|
||||||
|
|
||||||
|
if openshiftOAuth {
|
||||||
|
CreateOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and watch Che server resources
|
||||||
|
cheConfigMap := CreateCheConfigMap(cheHost, keycloakURL)
|
||||||
|
err := sdk.Update(cheConfigMap)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to update Che configmap : %v", err)
|
||||||
|
}
|
||||||
|
if cheFlavor == "codeready" {
|
||||||
|
cheImage = util.GetEnv("CHE_IMAGE", "registry.access.redhat.com/codeready-workspaces-beta/server:latest")
|
||||||
|
}
|
||||||
|
cheDc, _ := CreateCheDeployment(cheImage)
|
||||||
|
err = sdk.Update(cheDc)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to update Che deployment : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
rbac "k8s.io/api/rbac/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRole(name string, resources []string, verbs []string) *rbac.Role {
|
||||||
|
labels := map[string]string{"app": "che"}
|
||||||
|
return &rbac.Role{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Role",
|
||||||
|
APIVersion: rbac.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Rules: []rbac.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
Resources: resources,
|
||||||
|
Verbs: verbs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNewRole(name string, resources []string, verbs []string) *rbac.Role {
|
||||||
|
role := newRole(name, resources, verbs)
|
||||||
|
if err := sdk.Create(role); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+name+" role : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
route "github.com/openshift/api/route/v1"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRoute(name string, serviceName string) *route.Route {
|
||||||
|
var labels = cheLabels
|
||||||
|
if name == "keycloak" {
|
||||||
|
labels = keycloakLabels
|
||||||
|
}
|
||||||
|
return &route.Route{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Route",
|
||||||
|
APIVersion: route.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Spec: route.RouteSpec{
|
||||||
|
To: route.RouteTargetReference{
|
||||||
|
Kind: "Service",
|
||||||
|
Name: serviceName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTlsRoute(name string, serviceName string) *route.Route {
|
||||||
|
var labels = cheLabels
|
||||||
|
if name == "keycloak" {
|
||||||
|
labels = keycloakLabels
|
||||||
|
}
|
||||||
|
return &route.Route{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Route",
|
||||||
|
APIVersion: route.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Spec: route.RouteSpec{
|
||||||
|
To: route.RouteTargetReference{
|
||||||
|
Kind: "Service",
|
||||||
|
Name: serviceName,
|
||||||
|
},
|
||||||
|
TLS: &route.TLSConfig{
|
||||||
|
InsecureEdgeTerminationPolicy: route.InsecureEdgeTerminationPolicyRedirect,
|
||||||
|
Termination: route.TLSTerminationEdge,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRoute(name string, toService string) (*route.Route, error) {
|
||||||
|
rt := newRoute(name, toService)
|
||||||
|
if tlsSupport {
|
||||||
|
rt = newTlsRoute(name, toService)
|
||||||
|
}
|
||||||
|
if err := sdk.Create(rt); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+name+" route : %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRouteIfNotExists(name string, toService string) (*route.Route, error) {
|
||||||
|
rt := newRoute(name, toService)
|
||||||
|
if tlsSupport {
|
||||||
|
rt = newTlsRoute(name, toService)
|
||||||
|
}
|
||||||
|
err := sdk.Get(rt)
|
||||||
|
if err != nil {
|
||||||
|
return createRoute(name, toService)
|
||||||
|
}
|
||||||
|
return rt, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSecret() *corev1.Secret {
|
||||||
|
cert := util.GetSelfSignedCert()
|
||||||
|
labels := map[string]string{"app": "che"}
|
||||||
|
return &corev1.Secret{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Secret",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "self-signed-certificate",
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"ca.crt": cert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCertSecret() (*corev1.Secret) {
|
||||||
|
secret := newSecret()
|
||||||
|
if err := sdk.Create(secret); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create Che secret : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/sdk"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func service(name string, labels map[string]string, portName string, portNumber int32) *corev1.Service {
|
||||||
|
|
||||||
|
return &corev1.Service{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Service",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Name: portName,
|
||||||
|
Port: portNumber,
|
||||||
|
Protocol: "TCP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Selector: labels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CreateService creates a service with a given name, port, selector and labels
|
||||||
|
func CreateService(name string, labels map[string]string, portName string, portNumber int32) *corev1.Service {
|
||||||
|
svc := service(name, labels, portName, portNumber)
|
||||||
|
if err := sdk.Create(svc); err != nil && !errors.IsAlreadyExists(err) {
|
||||||
|
logrus.Errorf("Failed to create "+name+" service : %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return svc
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/eclipse/che-operator/pkg/util"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// general config
|
||||||
|
cheFlavor = util.GetEnv("CHE_FLAVOR", "che")
|
||||||
|
namespace = util.GetNamespace()
|
||||||
|
protocol = "http"
|
||||||
|
wsprotocol = "ws"
|
||||||
|
cheHost string
|
||||||
|
tlsSupport = util.GetEnvBool("CHE_TLS_SUPPORT", false)
|
||||||
|
pvcStrategy = util.GetEnv("CHE_INFRA_KUBERNETES_PVC_STRATEGY", "common")
|
||||||
|
pvcClaimSize = util.GetEnv("CHE_INFRA_KUBERNETES_PVC_QUANTITY", "1Gi")
|
||||||
|
selfSignedCert = util.GetEnv("CHE_SELF__SIGNED__CERT", "")
|
||||||
|
openshiftOAuth = util.GetEnvBool("CHE_OPENSHIFT_OAUTH", false)
|
||||||
|
oauthSecret = util.GeneratePasswd(12)
|
||||||
|
oAuthClientName = "openshift-identity-provider-" + strings.ToLower(util.GeneratePasswd(4))
|
||||||
|
updateAdminPassword = util.GetEnvBool("CHE_UPDATE_CHE_ADMIN_PASSWORD", true)
|
||||||
|
|
||||||
|
// proxy config
|
||||||
|
|
||||||
|
cheWsmasterProxyJavaOptions = util.GetEnv("CHE_WORKSPACE_MASTER_PROXY_JAVA_OPTS", "")
|
||||||
|
cheWorkspaceProxyJavaOptions = util.GetEnv("CHE_WORKSPACE_PROXY_JAVA_OPTS", "")
|
||||||
|
cheWorkspaceHttpProxy = util.GetEnv("CHE_WORKSPACE_HTTP__PROXY", "")
|
||||||
|
cheWorkspaceHttpsProxy = util.GetEnv("CHE_WORKSPACE_HTTPS__PROXY", "")
|
||||||
|
cheWorkspaceNoProxy = util.GetEnv("CHE_WORKSPACE_NO__PROXY", "")
|
||||||
|
|
||||||
|
// plugin registry url
|
||||||
|
pluginRegistryUrl = util.GetEnv("CHE_WORKSPACE_PLUGIN__REGISTRY__URL", "https://che-plugin-registry.openshift.io")
|
||||||
|
|
||||||
|
// k8s specific config
|
||||||
|
|
||||||
|
ingressDomain = util.GetEnv("CHE_INFRA_KUBERNETES_INGRESS_DOMAIN", "192.168.42.114")
|
||||||
|
strategy = util.GetEnv("CHE_INFRA_KUBERNETES_SERVER__STRATEGY", "multi-host")
|
||||||
|
ingressClass = util.GetEnv("INGRESS_CLASS", "nginx")
|
||||||
|
tlsSecretName = util.GetEnv("CHE_INFRA_KUBERNETES_TLS__SECRET", "")
|
||||||
|
// postgres config
|
||||||
|
externalDb = util.GetEnvBool("CHE_EXTERNAL_DB", false)
|
||||||
|
postgresHostName = util.GetEnv("CHE_DB_HOSTNAME", "postgres")
|
||||||
|
postgresPort = util.GetEnv("CHE_DB_PORT", "5432")
|
||||||
|
chePostgresDb = util.GetEnv("CHE_DB_DATABASE", "dbche")
|
||||||
|
chePostgresUser = util.GetEnv("CHE_JDBC_USERNAME", "pgche")
|
||||||
|
chePostgresPassword = util.GetEnv("CHE_JDBC_PASSWORD", util.GeneratePasswd(12))
|
||||||
|
postgresAdminPassword = util.GeneratePasswd(12)
|
||||||
|
|
||||||
|
// Keycloak config
|
||||||
|
externalKeycloak = util.GetEnvBool("CHE_EXTERNAL_KEYCLOAK", false)
|
||||||
|
keycloakURL = util.GetEnv("CHE_KEYCLOAK_AUTH__SERVER__URL", "")
|
||||||
|
keycloakAdminUserName = util.GetEnv("CHE_KEYCLOAK_ADMIN_USERNAME", "admin")
|
||||||
|
keycloakAdminPassword = util.GetEnv("CHE_KEYCLOAK_ADMIN_PASSWORD", util.GeneratePasswd(12))
|
||||||
|
keycloakPostgresPassword = util.GeneratePasswd(10)
|
||||||
|
keycloakRealm = util.GetEnv("CHE_KEYCLOAK_REALM", cheFlavor)
|
||||||
|
keycloakClientId = util.GetEnv("CHE_KEYCLOAK_CLIENT__ID", cheFlavor+"-public")
|
||||||
|
|
||||||
|
cheImage = util.GetEnv("CHE_IMAGE", "eclipse/che-server:latest")
|
||||||
|
|
||||||
|
postgresLabels = map[string]string{"app": "postgres"}
|
||||||
|
keycloakLabels = map[string]string{"app": "keycloak"}
|
||||||
|
cheLabels = map[string]string{"app": "che"}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
// GetSelfSignedCert get content of provided self signed certificate
|
||||||
|
func GetSelfSignedCert() (certContent []byte) {
|
||||||
|
base64Cert := GetEnv("CHE_SELF__SIGNED__CERT","")
|
||||||
|
certContent, _ = base64.StdEncoding.DecodeString(base64Cert)
|
||||||
|
return certContent
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018 Red Hat, Inc.
|
||||||
|
// This program and the accompanying materials are made
|
||||||
|
// available under the terms of the Eclipse Public License 2.0
|
||||||
|
// which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0
|
||||||
|
//
|
||||||
|
// Contributors:
|
||||||
|
// Red Hat, Inc. - initial API and implementation
|
||||||
|
//
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/operator-framework/operator-sdk/pkg/k8sclient"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetEnvValue looks for env variables in Operator pod to configure Code Ready deployments
|
||||||
|
// with things like db users, passwords and deployment options in general. Envs are set in
|
||||||
|
// a ConfigMap at deploy/config.yaml. Find more details on deployment options in README.md
|
||||||
|
func GetEnv(key, defaultValue string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if len(value) == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvBool(key string, defaultValue bool) bool {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if len(value) == 0 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if value == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePasswd(stringLength int) (passwd string) {
|
||||||
|
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||||
|
"abcdefghijklmnopqrstuvwxyz" +
|
||||||
|
"0123456789")
|
||||||
|
length := stringLength
|
||||||
|
buf := make([]rune, length)
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = chars[rand.Intn(len(chars))]
|
||||||
|
}
|
||||||
|
passwd = string(buf)
|
||||||
|
return passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInfra() (infra string) {
|
||||||
|
// set infra via env var, for instance for testing purposes
|
||||||
|
infraEnv := os.Getenv("INFRA")
|
||||||
|
if len(infraEnv) != 0 {
|
||||||
|
infra = infraEnv
|
||||||
|
return infra
|
||||||
|
} else {
|
||||||
|
kubeClient := k8sclient.GetKubeClient()
|
||||||
|
serverGroups, _ := kubeClient.Discovery().ServerGroups()
|
||||||
|
apiGroups := serverGroups.Groups
|
||||||
|
|
||||||
|
for i := range apiGroups {
|
||||||
|
name := apiGroups[i].Name
|
||||||
|
if name == "route.openshift.io" {
|
||||||
|
infra = "openshift"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if infra == "" {
|
||||||
|
infra = "kubernetes"
|
||||||
|
}
|
||||||
|
return infra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNamespace() (currentNamespace string) {
|
||||||
|
|
||||||
|
namespace, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("Failed to find mounted file with namespace name %s. Using default namespace eclipse-che", err)
|
||||||
|
namespace = []byte("eclipse-che")
|
||||||
|
}
|
||||||
|
currentNamespace = string(namespace)
|
||||||
|
return currentNamespace
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# This is the official list of cloud authors for copyright purposes.
|
||||||
|
# This file is distinct from the CONTRIBUTORS files.
|
||||||
|
# See the latter for an explanation.
|
||||||
|
|
||||||
|
# Names should be added to this file as:
|
||||||
|
# Name or Organization <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
|
||||||
|
Filippo Valsorda <hi@filippo.io>
|
||||||
|
Google Inc.
|
||||||
|
Ingo Oeser <nightlyone@googlemail.com>
|
||||||
|
Palm Stone Games, Inc.
|
||||||
|
Paweł Knap <pawelknap88@gmail.com>
|
||||||
|
Péter Szilágyi <peterke@gmail.com>
|
||||||
|
Tyler Treat <ttreat31@gmail.com>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# People who have agreed to one of the CLAs and can contribute patches.
|
||||||
|
# The AUTHORS file lists the copyright holders; this file
|
||||||
|
# lists people. For example, Google employees are listed here
|
||||||
|
# but not in AUTHORS, because Google holds the copyright.
|
||||||
|
#
|
||||||
|
# https://developers.google.com/open-source/cla/individual
|
||||||
|
# https://developers.google.com/open-source/cla/corporate
|
||||||
|
#
|
||||||
|
# Names should be added to this file as:
|
||||||
|
# Name <email address>
|
||||||
|
|
||||||
|
# Keep the list alphabetically sorted.
|
||||||
|
|
||||||
|
Alexis Hunt <lexer@google.com>
|
||||||
|
Andreas Litt <andreas.litt@gmail.com>
|
||||||
|
Andrew Gerrand <adg@golang.org>
|
||||||
|
Brad Fitzpatrick <bradfitz@golang.org>
|
||||||
|
Burcu Dogan <jbd@google.com>
|
||||||
|
Dave Day <djd@golang.org>
|
||||||
|
David Sansome <me@davidsansome.com>
|
||||||
|
David Symonds <dsymonds@golang.org>
|
||||||
|
Filippo Valsorda <hi@filippo.io>
|
||||||
|
Glenn Lewis <gmlewis@google.com>
|
||||||
|
Ingo Oeser <nightlyone@googlemail.com>
|
||||||
|
James Hall <james.hall@shopify.com>
|
||||||
|
Johan Euphrosine <proppy@google.com>
|
||||||
|
Jonathan Amsterdam <jba@google.com>
|
||||||
|
Kunpei Sakai <namusyaka@gmail.com>
|
||||||
|
Luna Duclos <luna.duclos@palmstonegames.com>
|
||||||
|
Magnus Hiie <magnus.hiie@gmail.com>
|
||||||
|
Mario Castro <mariocaster@gmail.com>
|
||||||
|
Michael McGreevy <mcgreevy@golang.org>
|
||||||
|
Omar Jarjur <ojarjur@google.com>
|
||||||
|
Paweł Knap <pawelknap88@gmail.com>
|
||||||
|
Péter Szilágyi <peterke@gmail.com>
|
||||||
|
Sarah Adams <shadams@google.com>
|
||||||
|
Thanatat Tamtan <acoshift@gmail.com>
|
||||||
|
Toby Burress <kurin@google.com>
|
||||||
|
Tuo Shan <shantuo@google.com>
|
||||||
|
Tyler Treat <ttreat31@gmail.com>
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
@ -0,0 +1,503 @@
|
||||||
|
// Copyright 2014 Google LLC
|
||||||
|
//
|
||||||
|
// 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 metadata provides access to Google Compute Engine (GCE)
|
||||||
|
// metadata and API service accounts.
|
||||||
|
//
|
||||||
|
// This package is a wrapper around the GCE metadata service,
|
||||||
|
// as documented at https://developers.google.com/compute/docs/metadata.
|
||||||
|
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// metadataIP is the documented metadata server IP address.
|
||||||
|
metadataIP = "169.254.169.254"
|
||||||
|
|
||||||
|
// metadataHostEnv is the environment variable specifying the
|
||||||
|
// GCE metadata hostname. If empty, the default value of
|
||||||
|
// metadataIP ("169.254.169.254") is used instead.
|
||||||
|
// This is variable name is not defined by any spec, as far as
|
||||||
|
// I know; it was made up for the Go package.
|
||||||
|
metadataHostEnv = "GCE_METADATA_HOST"
|
||||||
|
|
||||||
|
userAgent = "gcloud-golang/0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachedValue struct {
|
||||||
|
k string
|
||||||
|
trim bool
|
||||||
|
mu sync.Mutex
|
||||||
|
v string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
projID = &cachedValue{k: "project/project-id", trim: true}
|
||||||
|
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
|
||||||
|
instID = &cachedValue{k: "instance/id", trim: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultClient = &Client{hc: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).Dial,
|
||||||
|
ResponseHeaderTimeout: 2 * time.Second,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
subscribeClient = &Client{hc: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).Dial,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotDefinedError is returned when requested metadata is not defined.
|
||||||
|
//
|
||||||
|
// The underlying string is the suffix after "/computeMetadata/v1/".
|
||||||
|
//
|
||||||
|
// This error is not returned if the value is defined to be the empty
|
||||||
|
// string.
|
||||||
|
type NotDefinedError string
|
||||||
|
|
||||||
|
func (suffix NotDefinedError) Error() string {
|
||||||
|
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cachedValue) get(cl *Client) (v string, err error) {
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.v != "" {
|
||||||
|
return c.v, nil
|
||||||
|
}
|
||||||
|
if c.trim {
|
||||||
|
v, err = cl.getTrimmed(c.k)
|
||||||
|
} else {
|
||||||
|
v, err = cl.Get(c.k)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
c.v = v
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
onGCEOnce sync.Once
|
||||||
|
onGCE bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnGCE reports whether this process is running on Google Compute Engine.
|
||||||
|
func OnGCE() bool {
|
||||||
|
onGCEOnce.Do(initOnGCE)
|
||||||
|
return onGCE
|
||||||
|
}
|
||||||
|
|
||||||
|
func initOnGCE() {
|
||||||
|
onGCE = testOnGCE()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOnGCE() bool {
|
||||||
|
// The user explicitly said they're on GCE, so trust them.
|
||||||
|
if os.Getenv(metadataHostEnv) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resc := make(chan bool, 2)
|
||||||
|
|
||||||
|
// Try two strategies in parallel.
|
||||||
|
// See https://github.com/GoogleCloudPlatform/google-cloud-go/issues/194
|
||||||
|
go func() {
|
||||||
|
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
res, err := ctxhttp.Do(ctx, defaultClient.hc, req)
|
||||||
|
if err != nil {
|
||||||
|
resc <- false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
resc <- res.Header.Get("Metadata-Flavor") == "Google"
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
addrs, err := net.LookupHost("metadata.google.internal")
|
||||||
|
if err != nil || len(addrs) == 0 {
|
||||||
|
resc <- false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resc <- strsContains(addrs, metadataIP)
|
||||||
|
}()
|
||||||
|
|
||||||
|
tryHarder := systemInfoSuggestsGCE()
|
||||||
|
if tryHarder {
|
||||||
|
res := <-resc
|
||||||
|
if res {
|
||||||
|
// The first strategy succeeded, so let's use it.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Wait for either the DNS or metadata server probe to
|
||||||
|
// contradict the other one and say we are running on
|
||||||
|
// GCE. Give it a lot of time to do so, since the system
|
||||||
|
// info already suggests we're running on a GCE BIOS.
|
||||||
|
timer := time.NewTimer(5 * time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case res = <-resc:
|
||||||
|
return res
|
||||||
|
case <-timer.C:
|
||||||
|
// Too slow. Who knows what this system is.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's no hint from the system info that we're running on
|
||||||
|
// GCE, so use the first probe's result as truth, whether it's
|
||||||
|
// true or false. The goal here is to optimize for speed for
|
||||||
|
// users who are NOT running on GCE. We can't assume that
|
||||||
|
// either a DNS lookup or an HTTP request to a blackholed IP
|
||||||
|
// address is fast. Worst case this should return when the
|
||||||
|
// metaClient's Transport.ResponseHeaderTimeout or
|
||||||
|
// Transport.Dial.Timeout fires (in two seconds).
|
||||||
|
return <-resc
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemInfoSuggestsGCE reports whether the local system (without
|
||||||
|
// doing network requests) suggests that we're running on GCE. If this
|
||||||
|
// returns true, testOnGCE tries a bit harder to reach its metadata
|
||||||
|
// server.
|
||||||
|
func systemInfoSuggestsGCE() bool {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
// We don't have any non-Linux clues available, at least yet.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
||||||
|
name := strings.TrimSpace(string(slurp))
|
||||||
|
return name == "Google" || name == "Google Compute Engine"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe calls Client.Subscribe on a client designed for subscribing (one with no
|
||||||
|
// ResponseHeaderTimeout).
|
||||||
|
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||||
|
return subscribeClient.Subscribe(suffix, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get calls Client.Get on the default client.
|
||||||
|
func Get(suffix string) (string, error) { return defaultClient.Get(suffix) }
|
||||||
|
|
||||||
|
// ProjectID returns the current instance's project ID string.
|
||||||
|
func ProjectID() (string, error) { return defaultClient.ProjectID() }
|
||||||
|
|
||||||
|
// NumericProjectID returns the current instance's numeric project ID.
|
||||||
|
func NumericProjectID() (string, error) { return defaultClient.NumericProjectID() }
|
||||||
|
|
||||||
|
// InternalIP returns the instance's primary internal IP address.
|
||||||
|
func InternalIP() (string, error) { return defaultClient.InternalIP() }
|
||||||
|
|
||||||
|
// ExternalIP returns the instance's primary external (public) IP address.
|
||||||
|
func ExternalIP() (string, error) { return defaultClient.ExternalIP() }
|
||||||
|
|
||||||
|
// Hostname returns the instance's hostname. This will be of the form
|
||||||
|
// "<instanceID>.c.<projID>.internal".
|
||||||
|
func Hostname() (string, error) { return defaultClient.Hostname() }
|
||||||
|
|
||||||
|
// InstanceTags returns the list of user-defined instance tags,
|
||||||
|
// assigned when initially creating a GCE instance.
|
||||||
|
func InstanceTags() ([]string, error) { return defaultClient.InstanceTags() }
|
||||||
|
|
||||||
|
// InstanceID returns the current VM's numeric instance ID.
|
||||||
|
func InstanceID() (string, error) { return defaultClient.InstanceID() }
|
||||||
|
|
||||||
|
// InstanceName returns the current VM's instance ID string.
|
||||||
|
func InstanceName() (string, error) { return defaultClient.InstanceName() }
|
||||||
|
|
||||||
|
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||||
|
func Zone() (string, error) { return defaultClient.Zone() }
|
||||||
|
|
||||||
|
// InstanceAttributes calls Client.InstanceAttributes on the default client.
|
||||||
|
func InstanceAttributes() ([]string, error) { return defaultClient.InstanceAttributes() }
|
||||||
|
|
||||||
|
// ProjectAttributes calls Client.ProjectAttributes on the default client.
|
||||||
|
func ProjectAttributes() ([]string, error) { return defaultClient.ProjectAttributes() }
|
||||||
|
|
||||||
|
// InstanceAttributeValue calls Client.InstanceAttributeValue on the default client.
|
||||||
|
func InstanceAttributeValue(attr string) (string, error) {
|
||||||
|
return defaultClient.InstanceAttributeValue(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectAttributeValue calls Client.ProjectAttributeValue on the default client.
|
||||||
|
func ProjectAttributeValue(attr string) (string, error) {
|
||||||
|
return defaultClient.ProjectAttributeValue(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes calls Client.Scopes on the default client.
|
||||||
|
func Scopes(serviceAccount string) ([]string, error) { return defaultClient.Scopes(serviceAccount) }
|
||||||
|
|
||||||
|
func strsContains(ss []string, s string) bool {
|
||||||
|
for _, v := range ss {
|
||||||
|
if v == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Client provides metadata.
|
||||||
|
type Client struct {
|
||||||
|
hc *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns a Client that can be used to fetch metadata. All HTTP requests
|
||||||
|
// will use the given http.Client instead of the default client.
|
||||||
|
func NewClient(c *http.Client) *Client {
|
||||||
|
return &Client{hc: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getETag returns a value from the metadata service as well as the associated ETag.
|
||||||
|
// This func is otherwise equivalent to Get.
|
||||||
|
func (c *Client) getETag(suffix string) (value, etag string, err error) {
|
||||||
|
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
||||||
|
// a container, which is an important use-case for local testing of cloud
|
||||||
|
// deployments. To enable spoofing of the metadata service, the environment
|
||||||
|
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
||||||
|
// requests shall go.
|
||||||
|
host := os.Getenv(metadataHostEnv)
|
||||||
|
if host == "" {
|
||||||
|
// Using 169.254.169.254 instead of "metadata" here because Go
|
||||||
|
// binaries built with the "netgo" tag and without cgo won't
|
||||||
|
// know the search suffix for "metadata" is
|
||||||
|
// ".google.internal", and this IP address is documented as
|
||||||
|
// being stable anyway.
|
||||||
|
host = metadataIP
|
||||||
|
}
|
||||||
|
url := "http://" + host + "/computeMetadata/v1/" + suffix
|
||||||
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Set("Metadata-Flavor", "Google")
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
res, err := c.hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode == http.StatusNotFound {
|
||||||
|
return "", "", NotDefinedError(suffix)
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return "", "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url)
|
||||||
|
}
|
||||||
|
all, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return string(all), res.Header.Get("Etag"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a value from the metadata service.
|
||||||
|
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||||
|
//
|
||||||
|
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
||||||
|
// 169.254.169.254 will be used instead.
|
||||||
|
//
|
||||||
|
// If the requested metadata is not defined, the returned error will
|
||||||
|
// be of type NotDefinedError.
|
||||||
|
func (c *Client) Get(suffix string) (string, error) {
|
||||||
|
val, _, err := c.getETag(suffix)
|
||||||
|
return val, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getTrimmed(suffix string) (s string, err error) {
|
||||||
|
s, err = c.Get(suffix)
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) lines(suffix string) ([]string, error) {
|
||||||
|
j, err := c.Get(suffix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := strings.Split(strings.TrimSpace(j), "\n")
|
||||||
|
for i := range s {
|
||||||
|
s[i] = strings.TrimSpace(s[i])
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectID returns the current instance's project ID string.
|
||||||
|
func (c *Client) ProjectID() (string, error) { return projID.get(c) }
|
||||||
|
|
||||||
|
// NumericProjectID returns the current instance's numeric project ID.
|
||||||
|
func (c *Client) NumericProjectID() (string, error) { return projNum.get(c) }
|
||||||
|
|
||||||
|
// InstanceID returns the current VM's numeric instance ID.
|
||||||
|
func (c *Client) InstanceID() (string, error) { return instID.get(c) }
|
||||||
|
|
||||||
|
// InternalIP returns the instance's primary internal IP address.
|
||||||
|
func (c *Client) InternalIP() (string, error) {
|
||||||
|
return c.getTrimmed("instance/network-interfaces/0/ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalIP returns the instance's primary external (public) IP address.
|
||||||
|
func (c *Client) ExternalIP() (string, error) {
|
||||||
|
return c.getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns the instance's hostname. This will be of the form
|
||||||
|
// "<instanceID>.c.<projID>.internal".
|
||||||
|
func (c *Client) Hostname() (string, error) {
|
||||||
|
return c.getTrimmed("instance/hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceTags returns the list of user-defined instance tags,
|
||||||
|
// assigned when initially creating a GCE instance.
|
||||||
|
func (c *Client) InstanceTags() ([]string, error) {
|
||||||
|
var s []string
|
||||||
|
j, err := c.Get("instance/tags")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceName returns the current VM's instance ID string.
|
||||||
|
func (c *Client) InstanceName() (string, error) {
|
||||||
|
host, err := c.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.Split(host, ".")[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone returns the current VM's zone, such as "us-central1-b".
|
||||||
|
func (c *Client) Zone() (string, error) {
|
||||||
|
zone, err := c.getTrimmed("instance/zone")
|
||||||
|
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return zone[strings.LastIndex(zone, "/")+1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceAttributes returns the list of user-defined attributes,
|
||||||
|
// assigned when initially creating a GCE VM instance. The value of an
|
||||||
|
// attribute can be obtained with InstanceAttributeValue.
|
||||||
|
func (c *Client) InstanceAttributes() ([]string, error) { return c.lines("instance/attributes/") }
|
||||||
|
|
||||||
|
// ProjectAttributes returns the list of user-defined attributes
|
||||||
|
// applying to the project as a whole, not just this VM. The value of
|
||||||
|
// an attribute can be obtained with ProjectAttributeValue.
|
||||||
|
func (c *Client) ProjectAttributes() ([]string, error) { return c.lines("project/attributes/") }
|
||||||
|
|
||||||
|
// InstanceAttributeValue returns the value of the provided VM
|
||||||
|
// instance attribute.
|
||||||
|
//
|
||||||
|
// If the requested attribute is not defined, the returned error will
|
||||||
|
// be of type NotDefinedError.
|
||||||
|
//
|
||||||
|
// InstanceAttributeValue may return ("", nil) if the attribute was
|
||||||
|
// defined to be the empty string.
|
||||||
|
func (c *Client) InstanceAttributeValue(attr string) (string, error) {
|
||||||
|
return c.Get("instance/attributes/" + attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectAttributeValue returns the value of the provided
|
||||||
|
// project attribute.
|
||||||
|
//
|
||||||
|
// If the requested attribute is not defined, the returned error will
|
||||||
|
// be of type NotDefinedError.
|
||||||
|
//
|
||||||
|
// ProjectAttributeValue may return ("", nil) if the attribute was
|
||||||
|
// defined to be the empty string.
|
||||||
|
func (c *Client) ProjectAttributeValue(attr string) (string, error) {
|
||||||
|
return c.Get("project/attributes/" + attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes returns the service account scopes for the given account.
|
||||||
|
// The account may be empty or the string "default" to use the instance's
|
||||||
|
// main account.
|
||||||
|
func (c *Client) Scopes(serviceAccount string) ([]string, error) {
|
||||||
|
if serviceAccount == "" {
|
||||||
|
serviceAccount = "default"
|
||||||
|
}
|
||||||
|
return c.lines("instance/service-accounts/" + serviceAccount + "/scopes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribes to a value from the metadata service.
|
||||||
|
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
||||||
|
// The suffix may contain query parameters.
|
||||||
|
//
|
||||||
|
// Subscribe calls fn with the latest metadata value indicated by the provided
|
||||||
|
// suffix. If the metadata value is deleted, fn is called with the empty string
|
||||||
|
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
||||||
|
// is deleted. Subscribe returns the error value returned from the last call to
|
||||||
|
// fn, which may be nil when ok == false.
|
||||||
|
func (c *Client) Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
||||||
|
const failedSubscribeSleep = time.Second * 5
|
||||||
|
|
||||||
|
// First check to see if the metadata value exists at all.
|
||||||
|
val, lastETag, err := c.getETag(suffix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(val, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
if strings.ContainsRune(suffix, '?') {
|
||||||
|
suffix += "&wait_for_change=true&last_etag="
|
||||||
|
} else {
|
||||||
|
suffix += "?wait_for_change=true&last_etag="
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
val, etag, err := c.getETag(suffix + url.QueryEscape(lastETag))
|
||||||
|
if err != nil {
|
||||||
|
if _, deleted := err.(NotDefinedError); !deleted {
|
||||||
|
time.Sleep(failedSubscribeSleep)
|
||||||
|
continue // Retry on other errors.
|
||||||
|
}
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
lastETag = etag
|
||||||
|
|
||||||
|
if err := fn(val, ok); err != nil || !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
Copyright (c) 2012, Martin Angers
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
@ -0,0 +1,379 @@
|
||||||
|
/*
|
||||||
|
Package purell offers URL normalization as described on the wikipedia page:
|
||||||
|
http://en.wikipedia.org/wiki/URL_normalization
|
||||||
|
*/
|
||||||
|
package purell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/urlesc"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
"golang.org/x/text/width"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A set of normalization flags determines how a URL will
|
||||||
|
// be normalized.
|
||||||
|
type NormalizationFlags uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Safe normalizations
|
||||||
|
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
|
||||||
|
FlagLowercaseHost // http://HOST -> http://host
|
||||||
|
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
|
||||||
|
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
|
||||||
|
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
|
||||||
|
FlagRemoveDefaultPort // http://host:80 -> http://host
|
||||||
|
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
|
||||||
|
|
||||||
|
// Usually safe normalizations
|
||||||
|
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
|
||||||
|
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
|
||||||
|
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
|
||||||
|
|
||||||
|
// Unsafe normalizations
|
||||||
|
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
|
||||||
|
FlagRemoveFragment // http://host/path#fragment -> http://host/path
|
||||||
|
FlagForceHTTP // https://host -> http://host
|
||||||
|
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
|
||||||
|
FlagRemoveWWW // http://www.host/ -> http://host/
|
||||||
|
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
|
||||||
|
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
|
||||||
|
|
||||||
|
// Normalizations not in the wikipedia article, required to cover tests cases
|
||||||
|
// submitted by jehiah
|
||||||
|
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
|
||||||
|
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
|
||||||
|
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
|
||||||
|
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
|
||||||
|
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
|
||||||
|
|
||||||
|
// Convenience set of safe normalizations
|
||||||
|
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
|
||||||
|
|
||||||
|
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
|
||||||
|
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
|
||||||
|
|
||||||
|
// Convenience set of usually safe normalizations (includes FlagsSafe)
|
||||||
|
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
|
||||||
|
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
|
||||||
|
|
||||||
|
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
|
||||||
|
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
|
||||||
|
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
|
||||||
|
|
||||||
|
// Convenience set of all available flags
|
||||||
|
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHttpPort = ":80"
|
||||||
|
defaultHttpsPort = ":443"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regular expressions used by the normalizations
|
||||||
|
var rxPort = regexp.MustCompile(`(:\d+)/?$`)
|
||||||
|
var rxDirIndex = regexp.MustCompile(`(^|/)((?:default|index)\.\w{1,4})$`)
|
||||||
|
var rxDupSlashes = regexp.MustCompile(`/{2,}`)
|
||||||
|
var rxDWORDHost = regexp.MustCompile(`^(\d+)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxOctalHost = regexp.MustCompile(`^(0\d*)\.(0\d*)\.(0\d*)\.(0\d*)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxHexHost = regexp.MustCompile(`^0x([0-9A-Fa-f]+)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxHostDots = regexp.MustCompile(`^(.+?)(:\d+)?$`)
|
||||||
|
var rxEmptyPort = regexp.MustCompile(`:+$`)
|
||||||
|
|
||||||
|
// Map of flags to implementation function.
|
||||||
|
// FlagDecodeUnnecessaryEscapes has no action, since it is done automatically
|
||||||
|
// by parsing the string as an URL. Same for FlagUppercaseEscapes and FlagRemoveEmptyQuerySeparator.
|
||||||
|
|
||||||
|
// Since maps have undefined traversing order, make a slice of ordered keys
|
||||||
|
var flagsOrder = []NormalizationFlags{
|
||||||
|
FlagLowercaseScheme,
|
||||||
|
FlagLowercaseHost,
|
||||||
|
FlagRemoveDefaultPort,
|
||||||
|
FlagRemoveDirectoryIndex,
|
||||||
|
FlagRemoveDotSegments,
|
||||||
|
FlagRemoveFragment,
|
||||||
|
FlagForceHTTP, // Must be after remove default port (because https=443/http=80)
|
||||||
|
FlagRemoveDuplicateSlashes,
|
||||||
|
FlagRemoveWWW,
|
||||||
|
FlagAddWWW,
|
||||||
|
FlagSortQuery,
|
||||||
|
FlagDecodeDWORDHost,
|
||||||
|
FlagDecodeOctalHost,
|
||||||
|
FlagDecodeHexHost,
|
||||||
|
FlagRemoveUnnecessaryHostDots,
|
||||||
|
FlagRemoveEmptyPortSeparator,
|
||||||
|
FlagRemoveTrailingSlash, // These two (add/remove trailing slash) must be last
|
||||||
|
FlagAddTrailingSlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and then the map, where order is unimportant
|
||||||
|
var flags = map[NormalizationFlags]func(*url.URL){
|
||||||
|
FlagLowercaseScheme: lowercaseScheme,
|
||||||
|
FlagLowercaseHost: lowercaseHost,
|
||||||
|
FlagRemoveDefaultPort: removeDefaultPort,
|
||||||
|
FlagRemoveDirectoryIndex: removeDirectoryIndex,
|
||||||
|
FlagRemoveDotSegments: removeDotSegments,
|
||||||
|
FlagRemoveFragment: removeFragment,
|
||||||
|
FlagForceHTTP: forceHTTP,
|
||||||
|
FlagRemoveDuplicateSlashes: removeDuplicateSlashes,
|
||||||
|
FlagRemoveWWW: removeWWW,
|
||||||
|
FlagAddWWW: addWWW,
|
||||||
|
FlagSortQuery: sortQuery,
|
||||||
|
FlagDecodeDWORDHost: decodeDWORDHost,
|
||||||
|
FlagDecodeOctalHost: decodeOctalHost,
|
||||||
|
FlagDecodeHexHost: decodeHexHost,
|
||||||
|
FlagRemoveUnnecessaryHostDots: removeUnncessaryHostDots,
|
||||||
|
FlagRemoveEmptyPortSeparator: removeEmptyPortSeparator,
|
||||||
|
FlagRemoveTrailingSlash: removeTrailingSlash,
|
||||||
|
FlagAddTrailingSlash: addTrailingSlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustNormalizeURLString returns the normalized string, and panics if an error occurs.
|
||||||
|
// It takes an URL string as input, as well as the normalization flags.
|
||||||
|
func MustNormalizeURLString(u string, f NormalizationFlags) string {
|
||||||
|
result, e := NormalizeURLString(u, f)
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object.
|
||||||
|
// It takes an URL string as input, as well as the normalization flags.
|
||||||
|
func NormalizeURLString(u string, f NormalizationFlags) (string, error) {
|
||||||
|
parsed, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f&FlagLowercaseHost == FlagLowercaseHost {
|
||||||
|
parsed.Host = strings.ToLower(parsed.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The idna package doesn't fully conform to RFC 5895
|
||||||
|
// (https://tools.ietf.org/html/rfc5895), so we do it here.
|
||||||
|
// Taken from Go 1.8 cycle source, courtesy of bradfitz.
|
||||||
|
// TODO: Remove when (if?) idna package conforms to RFC 5895.
|
||||||
|
parsed.Host = width.Fold.String(parsed.Host)
|
||||||
|
parsed.Host = norm.NFC.String(parsed.Host)
|
||||||
|
if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NormalizeURL(parsed, f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeURL returns the normalized string.
|
||||||
|
// It takes a parsed URL object as input, as well as the normalization flags.
|
||||||
|
func NormalizeURL(u *url.URL, f NormalizationFlags) string {
|
||||||
|
for _, k := range flagsOrder {
|
||||||
|
if f&k == k {
|
||||||
|
flags[k](u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlesc.Escape(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowercaseScheme(u *url.URL) {
|
||||||
|
if len(u.Scheme) > 0 {
|
||||||
|
u.Scheme = strings.ToLower(u.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowercaseHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
u.Host = strings.ToLower(u.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDefaultPort(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
scheme := strings.ToLower(u.Scheme)
|
||||||
|
u.Host = rxPort.ReplaceAllStringFunc(u.Host, func(val string) string {
|
||||||
|
if (scheme == "http" && val == defaultHttpPort) || (scheme == "https" && val == defaultHttpsPort) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTrailingSlash(u *url.URL) {
|
||||||
|
if l := len(u.Path); l > 0 {
|
||||||
|
if strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path = u.Path[:l-1]
|
||||||
|
}
|
||||||
|
} else if l = len(u.Host); l > 0 {
|
||||||
|
if strings.HasSuffix(u.Host, "/") {
|
||||||
|
u.Host = u.Host[:l-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTrailingSlash(u *url.URL) {
|
||||||
|
if l := len(u.Path); l > 0 {
|
||||||
|
if !strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path += "/"
|
||||||
|
}
|
||||||
|
} else if l = len(u.Host); l > 0 {
|
||||||
|
if !strings.HasSuffix(u.Host, "/") {
|
||||||
|
u.Host += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDotSegments(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
var dotFree []string
|
||||||
|
var lastIsDot bool
|
||||||
|
|
||||||
|
sections := strings.Split(u.Path, "/")
|
||||||
|
for _, s := range sections {
|
||||||
|
if s == ".." {
|
||||||
|
if len(dotFree) > 0 {
|
||||||
|
dotFree = dotFree[:len(dotFree)-1]
|
||||||
|
}
|
||||||
|
} else if s != "." {
|
||||||
|
dotFree = append(dotFree, s)
|
||||||
|
}
|
||||||
|
lastIsDot = (s == "." || s == "..")
|
||||||
|
}
|
||||||
|
// Special case if host does not end with / and new path does not begin with /
|
||||||
|
u.Path = strings.Join(dotFree, "/")
|
||||||
|
if u.Host != "" && !strings.HasSuffix(u.Host, "/") && !strings.HasPrefix(u.Path, "/") {
|
||||||
|
u.Path = "/" + u.Path
|
||||||
|
}
|
||||||
|
// Special case if the last segment was a dot, make sure the path ends with a slash
|
||||||
|
if lastIsDot && !strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDirectoryIndex(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
u.Path = rxDirIndex.ReplaceAllString(u.Path, "$1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFragment(u *url.URL) {
|
||||||
|
u.Fragment = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceHTTP(u *url.URL) {
|
||||||
|
if strings.ToLower(u.Scheme) == "https" {
|
||||||
|
u.Scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDuplicateSlashes(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
u.Path = rxDupSlashes.ReplaceAllString(u.Path, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWWW(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 && strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||||
|
u.Host = u.Host[4:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWWW(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 && !strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||||
|
u.Host = "www." + u.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortQuery(u *url.URL) {
|
||||||
|
q := u.Query()
|
||||||
|
|
||||||
|
if len(q) > 0 {
|
||||||
|
arKeys := make([]string, len(q))
|
||||||
|
i := 0
|
||||||
|
for k, _ := range q {
|
||||||
|
arKeys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sort.Strings(arKeys)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, k := range arKeys {
|
||||||
|
sort.Strings(q[k])
|
||||||
|
for _, v := range q[k] {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteRune('&')
|
||||||
|
}
|
||||||
|
buf.WriteString(fmt.Sprintf("%s=%s", k, urlesc.QueryEscape(v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the raw query string
|
||||||
|
u.RawQuery = buf.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDWORDHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxDWORDHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||||
|
var parts [4]int64
|
||||||
|
|
||||||
|
dword, _ := strconv.ParseInt(matches[1], 10, 0)
|
||||||
|
for i, shift := range []uint{24, 16, 8, 0} {
|
||||||
|
parts[i] = dword >> shift & 0xFF
|
||||||
|
}
|
||||||
|
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeOctalHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxOctalHost.FindStringSubmatch(u.Host); len(matches) > 5 {
|
||||||
|
var parts [4]int64
|
||||||
|
|
||||||
|
for i := 1; i <= 4; i++ {
|
||||||
|
parts[i-1], _ = strconv.ParseInt(matches[i], 8, 0)
|
||||||
|
}
|
||||||
|
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHexHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxHexHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||||
|
// Conversion is safe because of regex validation
|
||||||
|
parsed, _ := strconv.ParseInt(matches[1], 16, 0)
|
||||||
|
// Set host as DWORD (base 10) encoded host
|
||||||
|
u.Host = fmt.Sprintf("%d%s", parsed, matches[2])
|
||||||
|
// The rest is the same as decoding a DWORD host
|
||||||
|
decodeDWORDHost(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeUnncessaryHostDots(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxHostDots.FindStringSubmatch(u.Host); len(matches) > 1 {
|
||||||
|
// Trim the leading and trailing dots
|
||||||
|
u.Host = strings.Trim(matches[1], ".")
|
||||||
|
if len(matches) > 2 {
|
||||||
|
u.Host += matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeEmptyPortSeparator(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
u.Host = rxEmptyPort.ReplaceAllString(u.Host, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package urlesc implements query escaping as per RFC 3986.
|
||||||
|
// It contains some parts of the net/url package, modified so as to allow
|
||||||
|
// some reserved characters incorrectly escaped by net/url.
|
||||||
|
// See https://github.com/golang/go/issues/5684
|
||||||
|
package urlesc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoding int
|
||||||
|
|
||||||
|
const (
|
||||||
|
encodePath encoding = 1 + iota
|
||||||
|
encodeUserPassword
|
||||||
|
encodeQueryComponent
|
||||||
|
encodeFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return true if the specified character should be escaped when
|
||||||
|
// appearing in a URL string, according to RFC 3986.
|
||||||
|
func shouldEscape(c byte, mode encoding) bool {
|
||||||
|
// §2.3 Unreserved characters (alphanum)
|
||||||
|
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case '-', '.', '_', '~': // §2.3 Unreserved characters (mark)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// §2.2 Reserved characters (reserved)
|
||||||
|
case ':', '/', '?', '#', '[', ']', '@', // gen-delims
|
||||||
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // sub-delims
|
||||||
|
// Different sections of the URL allow a few of
|
||||||
|
// the reserved characters to appear unescaped.
|
||||||
|
switch mode {
|
||||||
|
case encodePath: // §3.3
|
||||||
|
// The RFC allows sub-delims and : @.
|
||||||
|
// '/', '[' and ']' can be used to assign meaning to individual path
|
||||||
|
// segments. This package only manipulates the path as a whole,
|
||||||
|
// so we allow those as well. That leaves only ? and # to escape.
|
||||||
|
return c == '?' || c == '#'
|
||||||
|
|
||||||
|
case encodeUserPassword: // §3.2.1
|
||||||
|
// The RFC allows : and sub-delims in
|
||||||
|
// userinfo. The parsing of userinfo treats ':' as special so we must escape
|
||||||
|
// all the gen-delims.
|
||||||
|
return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
|
||||||
|
|
||||||
|
case encodeQueryComponent: // §3.4
|
||||||
|
// The RFC allows / and ?.
|
||||||
|
return c != '/' && c != '?'
|
||||||
|
|
||||||
|
case encodeFragment: // §4.1
|
||||||
|
// The RFC text is silent but the grammar allows
|
||||||
|
// everything, so escape nothing but #
|
||||||
|
return c == '#'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else must be escaped.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryEscape escapes the string so it can be safely placed
|
||||||
|
// inside a URL query.
|
||||||
|
func QueryEscape(s string) string {
|
||||||
|
return escape(s, encodeQueryComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func escape(s string, mode encoding) string {
|
||||||
|
spaceCount, hexCount := 0, 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
if shouldEscape(c, mode) {
|
||||||
|
if c == ' ' && mode == encodeQueryComponent {
|
||||||
|
spaceCount++
|
||||||
|
} else {
|
||||||
|
hexCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spaceCount == 0 && hexCount == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
t := make([]byte, len(s)+2*hexCount)
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch c := s[i]; {
|
||||||
|
case c == ' ' && mode == encodeQueryComponent:
|
||||||
|
t[j] = '+'
|
||||||
|
j++
|
||||||
|
case shouldEscape(c, mode):
|
||||||
|
t[j] = '%'
|
||||||
|
t[j+1] = "0123456789ABCDEF"[c>>4]
|
||||||
|
t[j+2] = "0123456789ABCDEF"[c&15]
|
||||||
|
j += 3
|
||||||
|
default:
|
||||||
|
t[j] = s[i]
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uiReplacer = strings.NewReplacer(
|
||||||
|
"%21", "!",
|
||||||
|
"%27", "'",
|
||||||
|
"%28", "(",
|
||||||
|
"%29", ")",
|
||||||
|
"%2A", "*",
|
||||||
|
)
|
||||||
|
|
||||||
|
// unescapeUserinfo unescapes some characters that need not to be escaped as per RFC3986.
|
||||||
|
func unescapeUserinfo(s string) string {
|
||||||
|
return uiReplacer.Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape reassembles the URL into a valid URL string.
|
||||||
|
// The general form of the result is one of:
|
||||||
|
//
|
||||||
|
// scheme:opaque
|
||||||
|
// scheme://userinfo@host/path?query#fragment
|
||||||
|
//
|
||||||
|
// If u.Opaque is non-empty, String uses the first form;
|
||||||
|
// otherwise it uses the second form.
|
||||||
|
//
|
||||||
|
// In the second form, the following rules apply:
|
||||||
|
// - if u.Scheme is empty, scheme: is omitted.
|
||||||
|
// - if u.User is nil, userinfo@ is omitted.
|
||||||
|
// - if u.Host is empty, host/ is omitted.
|
||||||
|
// - if u.Scheme and u.Host are empty and u.User is nil,
|
||||||
|
// the entire scheme://userinfo@host/ is omitted.
|
||||||
|
// - if u.Host is non-empty and u.Path begins with a /,
|
||||||
|
// the form host/path does not add its own /.
|
||||||
|
// - if u.RawQuery is empty, ?query is omitted.
|
||||||
|
// - if u.Fragment is empty, #fragment is omitted.
|
||||||
|
func Escape(u *url.URL) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if u.Scheme != "" {
|
||||||
|
buf.WriteString(u.Scheme)
|
||||||
|
buf.WriteByte(':')
|
||||||
|
}
|
||||||
|
if u.Opaque != "" {
|
||||||
|
buf.WriteString(u.Opaque)
|
||||||
|
} else {
|
||||||
|
if u.Scheme != "" || u.Host != "" || u.User != nil {
|
||||||
|
buf.WriteString("//")
|
||||||
|
if ui := u.User; ui != nil {
|
||||||
|
buf.WriteString(unescapeUserinfo(ui.String()))
|
||||||
|
buf.WriteByte('@')
|
||||||
|
}
|
||||||
|
if h := u.Host; h != "" {
|
||||||
|
buf.WriteString(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
|
||||||
|
buf.WriteByte('/')
|
||||||
|
}
|
||||||
|
buf.WriteString(escape(u.Path, encodePath))
|
||||||
|
}
|
||||||
|
if u.RawQuery != "" {
|
||||||
|
buf.WriteByte('?')
|
||||||
|
buf.WriteString(u.RawQuery)
|
||||||
|
}
|
||||||
|
if u.Fragment != "" {
|
||||||
|
buf.WriteByte('#')
|
||||||
|
buf.WriteString(escape(u.Fragment, encodeFragment))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (C) 2013 Blake Mizerany
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
// Package quantile computes approximate quantiles over an unbounded data
|
||||||
|
// stream within low memory and CPU bounds.
|
||||||
|
//
|
||||||
|
// A small amount of accuracy is traded to achieve the above properties.
|
||||||
|
//
|
||||||
|
// Multiple streams can be merged before calling Query to generate a single set
|
||||||
|
// of results. This is meaningful when the streams represent the same type of
|
||||||
|
// data. See Merge and Samples.
|
||||||
|
//
|
||||||
|
// For more detailed information about the algorithm used, see:
|
||||||
|
//
|
||||||
|
// Effective Computation of Biased Quantiles over Data Streams
|
||||||
|
//
|
||||||
|
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
|
||||||
|
package quantile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sample holds an observed value and meta information for compression. JSON
|
||||||
|
// tags have been added for convenience.
|
||||||
|
type Sample struct {
|
||||||
|
Value float64 `json:",string"`
|
||||||
|
Width float64 `json:",string"`
|
||||||
|
Delta float64 `json:",string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samples represents a slice of samples. It implements sort.Interface.
|
||||||
|
type Samples []Sample
|
||||||
|
|
||||||
|
func (a Samples) Len() int { return len(a) }
|
||||||
|
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
|
||||||
|
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
|
||||||
|
type invariant func(s *stream, r float64) float64
|
||||||
|
|
||||||
|
// NewLowBiased returns an initialized Stream for low-biased quantiles
|
||||||
|
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||||
|
// error guarantees can still be given even for the lower ranks of the data
|
||||||
|
// distribution.
|
||||||
|
//
|
||||||
|
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||||
|
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||||
|
// properties.
|
||||||
|
func NewLowBiased(epsilon float64) *Stream {
|
||||||
|
ƒ := func(s *stream, r float64) float64 {
|
||||||
|
return 2 * epsilon * r
|
||||||
|
}
|
||||||
|
return newStream(ƒ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHighBiased returns an initialized Stream for high-biased quantiles
|
||||||
|
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||||
|
// error guarantees can still be given even for the higher ranks of the data
|
||||||
|
// distribution.
|
||||||
|
//
|
||||||
|
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||||
|
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||||
|
// properties.
|
||||||
|
func NewHighBiased(epsilon float64) *Stream {
|
||||||
|
ƒ := func(s *stream, r float64) float64 {
|
||||||
|
return 2 * epsilon * (s.n - r)
|
||||||
|
}
|
||||||
|
return newStream(ƒ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTargeted returns an initialized Stream concerned with a particular set of
|
||||||
|
// quantile values that are supplied a priori. Knowing these a priori reduces
|
||||||
|
// space and computation time. The targets map maps the desired quantiles to
|
||||||
|
// their absolute errors, i.e. the true quantile of a value returned by a query
|
||||||
|
// is guaranteed to be within (Quantile±Epsilon).
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
|
||||||
|
func NewTargeted(targetMap map[float64]float64) *Stream {
|
||||||
|
// Convert map to slice to avoid slow iterations on a map.
|
||||||
|
// ƒ is called on the hot path, so converting the map to a slice
|
||||||
|
// beforehand results in significant CPU savings.
|
||||||
|
targets := targetMapToSlice(targetMap)
|
||||||
|
|
||||||
|
ƒ := func(s *stream, r float64) float64 {
|
||||||
|
var m = math.MaxFloat64
|
||||||
|
var f float64
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.quantile*s.n <= r {
|
||||||
|
f = (2 * t.epsilon * r) / t.quantile
|
||||||
|
} else {
|
||||||
|
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
|
||||||
|
}
|
||||||
|
if f < m {
|
||||||
|
m = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return newStream(ƒ)
|
||||||
|
}
|
||||||
|
|
||||||
|
type target struct {
|
||||||
|
quantile float64
|
||||||
|
epsilon float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func targetMapToSlice(targetMap map[float64]float64) []target {
|
||||||
|
targets := make([]target, 0, len(targetMap))
|
||||||
|
|
||||||
|
for quantile, epsilon := range targetMap {
|
||||||
|
t := target{
|
||||||
|
quantile: quantile,
|
||||||
|
epsilon: epsilon,
|
||||||
|
}
|
||||||
|
targets = append(targets, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
|
||||||
|
// design. Take care when using across multiple goroutines.
|
||||||
|
type Stream struct {
|
||||||
|
*stream
|
||||||
|
b Samples
|
||||||
|
sorted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStream(ƒ invariant) *Stream {
|
||||||
|
x := &stream{ƒ: ƒ}
|
||||||
|
return &Stream{x, make(Samples, 0, 500), true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts v into the stream.
|
||||||
|
func (s *Stream) Insert(v float64) {
|
||||||
|
s.insert(Sample{Value: v, Width: 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) insert(sample Sample) {
|
||||||
|
s.b = append(s.b, sample)
|
||||||
|
s.sorted = false
|
||||||
|
if len(s.b) == cap(s.b) {
|
||||||
|
s.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the computed qth percentiles value. If s was created with
|
||||||
|
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
|
||||||
|
// will return an unspecified result.
|
||||||
|
func (s *Stream) Query(q float64) float64 {
|
||||||
|
if !s.flushed() {
|
||||||
|
// Fast path when there hasn't been enough data for a flush;
|
||||||
|
// this also yields better accuracy for small sets of data.
|
||||||
|
l := len(s.b)
|
||||||
|
if l == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
i := int(math.Ceil(float64(l) * q))
|
||||||
|
if i > 0 {
|
||||||
|
i -= 1
|
||||||
|
}
|
||||||
|
s.maybeSort()
|
||||||
|
return s.b[i].Value
|
||||||
|
}
|
||||||
|
s.flush()
|
||||||
|
return s.stream.query(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges samples into the underlying streams samples. This is handy when
|
||||||
|
// merging multiple streams from separate threads, database shards, etc.
|
||||||
|
//
|
||||||
|
// ATTENTION: This method is broken and does not yield correct results. The
|
||||||
|
// underlying algorithm is not capable of merging streams correctly.
|
||||||
|
func (s *Stream) Merge(samples Samples) {
|
||||||
|
sort.Sort(samples)
|
||||||
|
s.stream.merge(samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset reinitializes and clears the list reusing the samples buffer memory.
|
||||||
|
func (s *Stream) Reset() {
|
||||||
|
s.stream.reset()
|
||||||
|
s.b = s.b[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samples returns stream samples held by s.
|
||||||
|
func (s *Stream) Samples() Samples {
|
||||||
|
if !s.flushed() {
|
||||||
|
return s.b
|
||||||
|
}
|
||||||
|
s.flush()
|
||||||
|
return s.stream.samples()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the total number of samples observed in the stream
|
||||||
|
// since initialization.
|
||||||
|
func (s *Stream) Count() int {
|
||||||
|
return len(s.b) + s.stream.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) flush() {
|
||||||
|
s.maybeSort()
|
||||||
|
s.stream.merge(s.b)
|
||||||
|
s.b = s.b[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) maybeSort() {
|
||||||
|
if !s.sorted {
|
||||||
|
s.sorted = true
|
||||||
|
sort.Sort(s.b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) flushed() bool {
|
||||||
|
return len(s.stream.l) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type stream struct {
|
||||||
|
n float64
|
||||||
|
l []Sample
|
||||||
|
ƒ invariant
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) reset() {
|
||||||
|
s.l = s.l[:0]
|
||||||
|
s.n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) insert(v float64) {
|
||||||
|
s.merge(Samples{{v, 1, 0}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) merge(samples Samples) {
|
||||||
|
// TODO(beorn7): This tries to merge not only individual samples, but
|
||||||
|
// whole summaries. The paper doesn't mention merging summaries at
|
||||||
|
// all. Unittests show that the merging is inaccurate. Find out how to
|
||||||
|
// do merges properly.
|
||||||
|
var r float64
|
||||||
|
i := 0
|
||||||
|
for _, sample := range samples {
|
||||||
|
for ; i < len(s.l); i++ {
|
||||||
|
c := s.l[i]
|
||||||
|
if c.Value > sample.Value {
|
||||||
|
// Insert at position i.
|
||||||
|
s.l = append(s.l, Sample{})
|
||||||
|
copy(s.l[i+1:], s.l[i:])
|
||||||
|
s.l[i] = Sample{
|
||||||
|
sample.Value,
|
||||||
|
sample.Width,
|
||||||
|
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
|
||||||
|
// TODO(beorn7): How to calculate delta correctly?
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
goto inserted
|
||||||
|
}
|
||||||
|
r += c.Width
|
||||||
|
}
|
||||||
|
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
|
||||||
|
i++
|
||||||
|
inserted:
|
||||||
|
s.n += sample.Width
|
||||||
|
r += sample.Width
|
||||||
|
}
|
||||||
|
s.compress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) count() int {
|
||||||
|
return int(s.n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) query(q float64) float64 {
|
||||||
|
t := math.Ceil(q * s.n)
|
||||||
|
t += math.Ceil(s.ƒ(s, t) / 2)
|
||||||
|
p := s.l[0]
|
||||||
|
var r float64
|
||||||
|
for _, c := range s.l[1:] {
|
||||||
|
r += p.Width
|
||||||
|
if r+c.Width+c.Delta > t {
|
||||||
|
return p.Value
|
||||||
|
}
|
||||||
|
p = c
|
||||||
|
}
|
||||||
|
return p.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) compress() {
|
||||||
|
if len(s.l) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x := s.l[len(s.l)-1]
|
||||||
|
xi := len(s.l) - 1
|
||||||
|
r := s.n - 1 - x.Width
|
||||||
|
|
||||||
|
for i := len(s.l) - 2; i >= 0; i-- {
|
||||||
|
c := s.l[i]
|
||||||
|
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
|
||||||
|
x.Width += c.Width
|
||||||
|
s.l[xi] = x
|
||||||
|
// Remove element at i.
|
||||||
|
copy(s.l[i:], s.l[i+1:])
|
||||||
|
s.l = s.l[:len(s.l)-1]
|
||||||
|
xi -= 1
|
||||||
|
} else {
|
||||||
|
x = c
|
||||||
|
xi = i
|
||||||
|
}
|
||||||
|
r -= c.Width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) samples() Samples {
|
||||||
|
samples := make(Samples, len(s.l))
|
||||||
|
copy(samples, s.l)
|
||||||
|
return samples
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||||
|
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// Go versions prior to 1.4 are disabled because they use a different layout
|
||||||
|
// for interfaces which make the implementation of unsafeReflectValue more complex.
|
||||||
|
// +build !js,!appengine,!safe,!disableunsafe,go1.4
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = false
|
||||||
|
|
||||||
|
// ptrSize is the size of a pointer on the current arch.
|
||||||
|
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
type flag uintptr
|
||||||
|
|
||||||
|
var (
|
||||||
|
// flagRO indicates whether the value field of a reflect.Value
|
||||||
|
// is read-only.
|
||||||
|
flagRO flag
|
||||||
|
|
||||||
|
// flagAddr indicates whether the address of the reflect.Value's
|
||||||
|
// value may be taken.
|
||||||
|
flagAddr flag
|
||||||
|
)
|
||||||
|
|
||||||
|
// flagKindMask holds the bits that make up the kind
|
||||||
|
// part of the flags field. In all the supported versions,
|
||||||
|
// it is in the lower 5 bits.
|
||||||
|
const flagKindMask = flag(0x1f)
|
||||||
|
|
||||||
|
// Different versions of Go have used different
|
||||||
|
// bit layouts for the flags type. This table
|
||||||
|
// records the known combinations.
|
||||||
|
var okFlags = []struct {
|
||||||
|
ro, addr flag
|
||||||
|
}{{
|
||||||
|
// From Go 1.4 to 1.5
|
||||||
|
ro: 1 << 5,
|
||||||
|
addr: 1 << 7,
|
||||||
|
}, {
|
||||||
|
// Up to Go tip.
|
||||||
|
ro: 1<<5 | 1<<6,
|
||||||
|
addr: 1 << 8,
|
||||||
|
}}
|
||||||
|
|
||||||
|
var flagValOffset = func() uintptr {
|
||||||
|
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||||
|
if !ok {
|
||||||
|
panic("reflect.Value has no flag field")
|
||||||
|
}
|
||||||
|
return field.Offset
|
||||||
|
}()
|
||||||
|
|
||||||
|
// flagField returns a pointer to the flag field of a reflect.Value.
|
||||||
|
func flagField(v *reflect.Value) *flag {
|
||||||
|
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||||
|
// the typical safety restrictions preventing access to unaddressable and
|
||||||
|
// unexported data. It works by digging the raw pointer to the underlying
|
||||||
|
// value out of the protected value and generating a new unprotected (unsafe)
|
||||||
|
// reflect.Value to it.
|
||||||
|
//
|
||||||
|
// This allows us to check for implementations of the Stringer and error
|
||||||
|
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||||
|
// inaccessible values such as unexported struct fields.
|
||||||
|
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||||
|
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
flagFieldPtr := flagField(&v)
|
||||||
|
*flagFieldPtr &^= flagRO
|
||||||
|
*flagFieldPtr |= flagAddr
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity checks against future reflect package changes
|
||||||
|
// to the type or semantics of the Value.flag field.
|
||||||
|
func init() {
|
||||||
|
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||||
|
if !ok {
|
||||||
|
panic("reflect.Value has no flag field")
|
||||||
|
}
|
||||||
|
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
|
||||||
|
panic("reflect.Value flag field has changed kind")
|
||||||
|
}
|
||||||
|
type t0 int
|
||||||
|
var t struct {
|
||||||
|
A t0
|
||||||
|
// t0 will have flagEmbedRO set.
|
||||||
|
t0
|
||||||
|
// a will have flagStickyRO set
|
||||||
|
a t0
|
||||||
|
}
|
||||||
|
vA := reflect.ValueOf(t).FieldByName("A")
|
||||||
|
va := reflect.ValueOf(t).FieldByName("a")
|
||||||
|
vt0 := reflect.ValueOf(t).FieldByName("t0")
|
||||||
|
|
||||||
|
// Infer flagRO from the difference between the flags
|
||||||
|
// for the (otherwise identical) fields in t.
|
||||||
|
flagPublic := *flagField(&vA)
|
||||||
|
flagWithRO := *flagField(&va) | *flagField(&vt0)
|
||||||
|
flagRO = flagPublic ^ flagWithRO
|
||||||
|
|
||||||
|
// Infer flagAddr from the difference between a value
|
||||||
|
// taken from a pointer and not.
|
||||||
|
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
|
||||||
|
flagNoPtr := *flagField(&vA)
|
||||||
|
flagPtr := *flagField(&vPtrA)
|
||||||
|
flagAddr = flagNoPtr ^ flagPtr
|
||||||
|
|
||||||
|
// Check that the inferred flags tally with one of the known versions.
|
||||||
|
for _, f := range okFlags {
|
||||||
|
if flagRO == f.ro && flagAddr == f.addr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("reflect.Value read-only flag has changed semantics")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||||
|
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build js appengine safe disableunsafe !go1.4
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// unsafeReflectValue typically converts the passed reflect.Value into a one
|
||||||
|
// that bypasses the typical safety restrictions preventing access to
|
||||||
|
// unaddressable and unexported data. However, doing this relies on access to
|
||||||
|
// the unsafe package. This is a stub version which simply returns the passed
|
||||||
|
// reflect.Value when the unsafe package is not available.
|
||||||
|
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,341 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Some constants in the form of bytes to avoid string overhead. This mirrors
|
||||||
|
// the technique used in the fmt package.
|
||||||
|
var (
|
||||||
|
panicBytes = []byte("(PANIC=")
|
||||||
|
plusBytes = []byte("+")
|
||||||
|
iBytes = []byte("i")
|
||||||
|
trueBytes = []byte("true")
|
||||||
|
falseBytes = []byte("false")
|
||||||
|
interfaceBytes = []byte("(interface {})")
|
||||||
|
commaNewlineBytes = []byte(",\n")
|
||||||
|
newlineBytes = []byte("\n")
|
||||||
|
openBraceBytes = []byte("{")
|
||||||
|
openBraceNewlineBytes = []byte("{\n")
|
||||||
|
closeBraceBytes = []byte("}")
|
||||||
|
asteriskBytes = []byte("*")
|
||||||
|
colonBytes = []byte(":")
|
||||||
|
colonSpaceBytes = []byte(": ")
|
||||||
|
openParenBytes = []byte("(")
|
||||||
|
closeParenBytes = []byte(")")
|
||||||
|
spaceBytes = []byte(" ")
|
||||||
|
pointerChainBytes = []byte("->")
|
||||||
|
nilAngleBytes = []byte("<nil>")
|
||||||
|
maxNewlineBytes = []byte("<max depth reached>\n")
|
||||||
|
maxShortBytes = []byte("<max>")
|
||||||
|
circularBytes = []byte("<already shown>")
|
||||||
|
circularShortBytes = []byte("<shown>")
|
||||||
|
invalidAngleBytes = []byte("<invalid>")
|
||||||
|
openBracketBytes = []byte("[")
|
||||||
|
closeBracketBytes = []byte("]")
|
||||||
|
percentBytes = []byte("%")
|
||||||
|
precisionBytes = []byte(".")
|
||||||
|
openAngleBytes = []byte("<")
|
||||||
|
closeAngleBytes = []byte(">")
|
||||||
|
openMapBytes = []byte("map[")
|
||||||
|
closeMapBytes = []byte("]")
|
||||||
|
lenEqualsBytes = []byte("len=")
|
||||||
|
capEqualsBytes = []byte("cap=")
|
||||||
|
)
|
||||||
|
|
||||||
|
// hexDigits is used to map a decimal value to a hex digit.
|
||||||
|
var hexDigits = "0123456789abcdef"
|
||||||
|
|
||||||
|
// catchPanic handles any panics that might occur during the handleMethods
|
||||||
|
// calls.
|
||||||
|
func catchPanic(w io.Writer, v reflect.Value) {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
w.Write(panicBytes)
|
||||||
|
fmt.Fprintf(w, "%v", err)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMethods attempts to call the Error and String methods on the underlying
|
||||||
|
// type the passed reflect.Value represents and outputes the result to Writer w.
|
||||||
|
//
|
||||||
|
// It handles panics in any called methods by catching and displaying the error
|
||||||
|
// as the formatted value.
|
||||||
|
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
|
||||||
|
// We need an interface to check if the type implements the error or
|
||||||
|
// Stringer interface. However, the reflect package won't give us an
|
||||||
|
// interface on certain things like unexported struct fields in order
|
||||||
|
// to enforce visibility rules. We use unsafe, when it's available,
|
||||||
|
// to bypass these restrictions since this package does not mutate the
|
||||||
|
// values.
|
||||||
|
if !v.CanInterface() {
|
||||||
|
if UnsafeDisabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose whether or not to do error and Stringer interface lookups against
|
||||||
|
// the base type or a pointer to the base type depending on settings.
|
||||||
|
// Technically calling one of these methods with a pointer receiver can
|
||||||
|
// mutate the value, however, types which choose to satisify an error or
|
||||||
|
// Stringer interface with a pointer receiver should not be mutating their
|
||||||
|
// state inside these interface methods.
|
||||||
|
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
if v.CanAddr() {
|
||||||
|
v = v.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it an error or Stringer?
|
||||||
|
switch iface := v.Interface().(type) {
|
||||||
|
case error:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// printBool outputs a boolean value as true or false to Writer w.
|
||||||
|
func printBool(w io.Writer, val bool) {
|
||||||
|
if val {
|
||||||
|
w.Write(trueBytes)
|
||||||
|
} else {
|
||||||
|
w.Write(falseBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printInt outputs a signed integer value to Writer w.
|
||||||
|
func printInt(w io.Writer, val int64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatInt(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printUint outputs an unsigned integer value to Writer w.
|
||||||
|
func printUint(w io.Writer, val uint64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatUint(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFloat outputs a floating point value using the specified precision,
|
||||||
|
// which is expected to be 32 or 64bit, to Writer w.
|
||||||
|
func printFloat(w io.Writer, val float64, precision int) {
|
||||||
|
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printComplex outputs a complex value using the specified float precision
|
||||||
|
// for the real and imaginary parts to Writer w.
|
||||||
|
func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||||
|
r := real(c)
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
|
||||||
|
i := imag(c)
|
||||||
|
if i >= 0 {
|
||||||
|
w.Write(plusBytes)
|
||||||
|
}
|
||||||
|
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
|
||||||
|
w.Write(iBytes)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
|
||||||
|
// prefix to Writer w.
|
||||||
|
func printHexPtr(w io.Writer, p uintptr) {
|
||||||
|
// Null pointer.
|
||||||
|
num := uint64(p)
|
||||||
|
if num == 0 {
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
|
||||||
|
buf := make([]byte, 18)
|
||||||
|
|
||||||
|
// It's simpler to construct the hex string right to left.
|
||||||
|
base := uint64(16)
|
||||||
|
i := len(buf) - 1
|
||||||
|
for num >= base {
|
||||||
|
buf[i] = hexDigits[num%base]
|
||||||
|
num /= base
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
buf[i] = hexDigits[num]
|
||||||
|
|
||||||
|
// Add '0x' prefix.
|
||||||
|
i--
|
||||||
|
buf[i] = 'x'
|
||||||
|
i--
|
||||||
|
buf[i] = '0'
|
||||||
|
|
||||||
|
// Strip unused leading bytes.
|
||||||
|
buf = buf[i:]
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
|
||||||
|
// elements to be sorted.
|
||||||
|
type valuesSorter struct {
|
||||||
|
values []reflect.Value
|
||||||
|
strings []string // either nil or same len and values
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValuesSorter initializes a valuesSorter instance, which holds a set of
|
||||||
|
// surrogate keys on which the data should be sorted. It uses flags in
|
||||||
|
// ConfigState to decide if and how to populate those surrogate keys.
|
||||||
|
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
|
||||||
|
vs := &valuesSorter{values: values, cs: cs}
|
||||||
|
if canSortSimply(vs.values[0].Kind()) {
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
if !cs.DisableMethods {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
if !handleMethods(cs, &b, vs.values[i]) {
|
||||||
|
vs.strings = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
vs.strings[i] = b.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vs.strings == nil && cs.SpewKeys {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
|
||||||
|
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
|
||||||
|
// directly, or whether it should be considered for sorting by surrogate keys
|
||||||
|
// (if the ConfigState allows it).
|
||||||
|
func canSortSimply(kind reflect.Kind) bool {
|
||||||
|
// This switch parallels valueSortLess, except for the default case.
|
||||||
|
switch kind {
|
||||||
|
case reflect.Bool:
|
||||||
|
return true
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return true
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return true
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
case reflect.String:
|
||||||
|
return true
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return true
|
||||||
|
case reflect.Array:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of values in the slice. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Len() int {
|
||||||
|
return len(s.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps the values at the passed indices. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Swap(i, j int) {
|
||||||
|
s.values[i], s.values[j] = s.values[j], s.values[i]
|
||||||
|
if s.strings != nil {
|
||||||
|
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueSortLess returns whether the first value should sort before the second
|
||||||
|
// value. It is used by valueSorter.Less as part of the sort.Interface
|
||||||
|
// implementation.
|
||||||
|
func valueSortLess(a, b reflect.Value) bool {
|
||||||
|
switch a.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return !a.Bool() && b.Bool()
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return a.Int() < b.Int()
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return a.Float() < b.Float()
|
||||||
|
case reflect.String:
|
||||||
|
return a.String() < b.String()
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Array:
|
||||||
|
// Compare the contents of both arrays.
|
||||||
|
l := a.Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
av := a.Index(i)
|
||||||
|
bv := b.Index(i)
|
||||||
|
if av.Interface() == bv.Interface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return valueSortLess(av, bv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.String() < b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less returns whether the value at index i should sort before the
|
||||||
|
// value at index j. It is part of the sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Less(i, j int) bool {
|
||||||
|
if s.strings == nil {
|
||||||
|
return valueSortLess(s.values[i], s.values[j])
|
||||||
|
}
|
||||||
|
return s.strings[i] < s.strings[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortValues is a sort function that handles both native types and any type that
|
||||||
|
// can be converted to error or Stringer. Other inputs are sorted according to
|
||||||
|
// their Value.String() value to ensure display stability.
|
||||||
|
func sortValues(values []reflect.Value, cs *ConfigState) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Sort(newValuesSorter(values, cs))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigState houses the configuration options used by spew to format and
|
||||||
|
// display values. There is a global instance, Config, that is used to control
|
||||||
|
// all top-level Formatter and Dump functionality. Each ConfigState instance
|
||||||
|
// provides methods equivalent to the top-level functions.
|
||||||
|
//
|
||||||
|
// The zero value for ConfigState provides no indentation. You would typically
|
||||||
|
// want to set it to a space or a tab.
|
||||||
|
//
|
||||||
|
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
|
||||||
|
// with default settings. See the documentation of NewDefaultConfig for default
|
||||||
|
// values.
|
||||||
|
type ConfigState struct {
|
||||||
|
// Indent specifies the string to use for each indentation level. The
|
||||||
|
// global config instance that all top-level functions use set this to a
|
||||||
|
// single space by default. If you would like more indentation, you might
|
||||||
|
// set this to a tab with "\t" or perhaps two spaces with " ".
|
||||||
|
Indent string
|
||||||
|
|
||||||
|
// MaxDepth controls the maximum number of levels to descend into nested
|
||||||
|
// data structures. The default, 0, means there is no limit.
|
||||||
|
//
|
||||||
|
// NOTE: Circular data structures are properly detected, so it is not
|
||||||
|
// necessary to set this value unless you specifically want to limit deeply
|
||||||
|
// nested data structures.
|
||||||
|
MaxDepth int
|
||||||
|
|
||||||
|
// DisableMethods specifies whether or not error and Stringer interfaces are
|
||||||
|
// invoked for types that implement them.
|
||||||
|
DisableMethods bool
|
||||||
|
|
||||||
|
// DisablePointerMethods specifies whether or not to check for and invoke
|
||||||
|
// error and Stringer interfaces on types which only accept a pointer
|
||||||
|
// receiver when the current type is not a pointer.
|
||||||
|
//
|
||||||
|
// NOTE: This might be an unsafe action since calling one of these methods
|
||||||
|
// with a pointer receiver could technically mutate the value, however,
|
||||||
|
// in practice, types which choose to satisify an error or Stringer
|
||||||
|
// interface with a pointer receiver should not be mutating their state
|
||||||
|
// inside these interface methods. As a result, this option relies on
|
||||||
|
// access to the unsafe package, so it will not have any effect when
|
||||||
|
// running in environments without access to the unsafe package such as
|
||||||
|
// Google App Engine or with the "safe" build tag specified.
|
||||||
|
DisablePointerMethods bool
|
||||||
|
|
||||||
|
// DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
// pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
DisablePointerAddresses bool
|
||||||
|
|
||||||
|
// DisableCapacities specifies whether to disable the printing of capacities
|
||||||
|
// for arrays, slices, maps and channels. This is useful when diffing
|
||||||
|
// data structures in tests.
|
||||||
|
DisableCapacities bool
|
||||||
|
|
||||||
|
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||||
|
// a custom error or Stringer interface is invoked. The default, false,
|
||||||
|
// means it will print the results of invoking the custom error or Stringer
|
||||||
|
// interface and return immediately instead of continuing to recurse into
|
||||||
|
// the internals of the data type.
|
||||||
|
//
|
||||||
|
// NOTE: This flag does not have any effect if method invocation is disabled
|
||||||
|
// via the DisableMethods or DisablePointerMethods options.
|
||||||
|
ContinueOnMethod bool
|
||||||
|
|
||||||
|
// SortKeys specifies map keys should be sorted before being printed. Use
|
||||||
|
// this to have a more deterministic, diffable output. Note that only
|
||||||
|
// native types (bool, int, uint, floats, uintptr and string) and types
|
||||||
|
// that support the error or Stringer interfaces (if methods are
|
||||||
|
// enabled) are supported, with other types sorted according to the
|
||||||
|
// reflect.Value.String() output which guarantees display stability.
|
||||||
|
SortKeys bool
|
||||||
|
|
||||||
|
// SpewKeys specifies that, as a last resort attempt, map keys should
|
||||||
|
// be spewed to strings and sorted by those strings. This is only
|
||||||
|
// considered if SortKeys is true.
|
||||||
|
SpewKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the active configuration of the top-level functions.
|
||||||
|
// The configuration can be changed by modifying the contents of spew.Config.
|
||||||
|
var Config = ConfigState{Indent: " "}
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the formatted string as a value that satisfies error. See NewFormatter
|
||||||
|
// for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a Formatter interface returned by c.NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
c.Printf, c.Println, or c.Printf.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(c, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(c, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by modifying the public members
|
||||||
|
of c. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) Dump(a ...interface{}) {
|
||||||
|
fdump(c, os.Stdout, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func (c *ConfigState) Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(c, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a spew Formatter interface using
|
||||||
|
// the ConfigState associated with s.
|
||||||
|
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = newFormatter(c, arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||||
|
//
|
||||||
|
// Indent: " "
|
||||||
|
// MaxDepth: 0
|
||||||
|
// DisableMethods: false
|
||||||
|
// DisablePointerMethods: false
|
||||||
|
// ContinueOnMethod: false
|
||||||
|
// SortKeys: false
|
||||||
|
func NewDefaultConfig() *ConfigState {
|
||||||
|
return &ConfigState{Indent: " "}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package spew implements a deep pretty printer for Go data structures to aid in
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
A quick overview of the additional features spew provides over the built-in
|
||||||
|
printing facilities for Go data types are as follows:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output (only when using
|
||||||
|
Dump style)
|
||||||
|
|
||||||
|
There are two different approaches spew allows for dumping Go data structures:
|
||||||
|
|
||||||
|
* Dump style which prints with newlines, customizable indentation,
|
||||||
|
and additional debug information such as types and all pointer addresses
|
||||||
|
used to indirect to the final value
|
||||||
|
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||||
|
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||||
|
similar to the default %v while providing the additional functionality
|
||||||
|
outlined above and passing unsupported format verbs such as %x and %q
|
||||||
|
along to fmt
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
|
||||||
|
This section demonstrates how to quickly get started with spew. See the
|
||||||
|
sections below for further details on formatting and configuration options.
|
||||||
|
|
||||||
|
To dump a variable with full newlines, indentation, type, and pointer
|
||||||
|
information use Dump, Fdump, or Sdump:
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Alternatively, if you would prefer to use format strings with a compacted inline
|
||||||
|
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||||
|
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||||
|
%#+v (adds types and pointer addresses):
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
Configuration Options
|
||||||
|
|
||||||
|
Configuration of spew is handled by fields in the ConfigState type. For
|
||||||
|
convenience, all of the top-level functions use a global state available
|
||||||
|
via the spew.Config global.
|
||||||
|
|
||||||
|
It is also possible to create a ConfigState instance that provides methods
|
||||||
|
equivalent to the top-level functions. This allows concurrent configuration
|
||||||
|
options. See the ConfigState documentation for more details.
|
||||||
|
|
||||||
|
The following configuration options are available:
|
||||||
|
* Indent
|
||||||
|
String to use for each indentation level for Dump functions.
|
||||||
|
It is a single space by default. A popular alternative is "\t".
|
||||||
|
|
||||||
|
* MaxDepth
|
||||||
|
Maximum number of levels to descend into nested data structures.
|
||||||
|
There is no limit by default.
|
||||||
|
|
||||||
|
* DisableMethods
|
||||||
|
Disables invocation of error and Stringer interface methods.
|
||||||
|
Method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerMethods
|
||||||
|
Disables invocation of error and Stringer interface methods on types
|
||||||
|
which only accept pointer receivers from non-pointer variables.
|
||||||
|
Pointer method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerAddresses
|
||||||
|
DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
|
||||||
|
* DisableCapacities
|
||||||
|
DisableCapacities specifies whether to disable the printing of
|
||||||
|
capacities for arrays, slices, maps and channels. This is useful when
|
||||||
|
diffing data structures in tests.
|
||||||
|
|
||||||
|
* ContinueOnMethod
|
||||||
|
Enables recursion into types after invoking error and Stringer interface
|
||||||
|
methods. Recursion after method invocation is disabled by default.
|
||||||
|
|
||||||
|
* SortKeys
|
||||||
|
Specifies map keys should be sorted before being printed. Use
|
||||||
|
this to have a more deterministic, diffable output. Note that
|
||||||
|
only native types (bool, int, uint, floats, uintptr and string)
|
||||||
|
and types which implement error or Stringer interfaces are
|
||||||
|
supported with other types sorted according to the
|
||||||
|
reflect.Value.String() output which guarantees display
|
||||||
|
stability. Natural map order is used by default.
|
||||||
|
|
||||||
|
* SpewKeys
|
||||||
|
Specifies that, as a last resort attempt, map keys should be
|
||||||
|
spewed to strings and sorted by those strings. This is only
|
||||||
|
considered if SortKeys is true.
|
||||||
|
|
||||||
|
Dump Usage
|
||||||
|
|
||||||
|
Simply call spew.Dump with a list of variables you want to dump:
|
||||||
|
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
You may also call spew.Fdump if you would prefer to output to an arbitrary
|
||||||
|
io.Writer. For example, to dump to standard error:
|
||||||
|
|
||||||
|
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
A third option is to call spew.Sdump to get the formatted output as a string:
|
||||||
|
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Sample Dump Output
|
||||||
|
|
||||||
|
See the Dump example for details on the setup of the types and variables being
|
||||||
|
shown here.
|
||||||
|
|
||||||
|
(main.Foo) {
|
||||||
|
unexportedField: (*main.Bar)(0xf84002e210)({
|
||||||
|
flag: (main.Flag) flagTwo,
|
||||||
|
data: (uintptr) <nil>
|
||||||
|
}),
|
||||||
|
ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
(string) (len=3) "one": (bool) true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||||
|
command as shown.
|
||||||
|
([]uint8) (len=32 cap=32) {
|
||||||
|
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||||
|
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||||
|
00000020 31 32 |12|
|
||||||
|
}
|
||||||
|
|
||||||
|
Custom Formatter
|
||||||
|
|
||||||
|
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||||
|
so that it integrates cleanly with standard fmt package printing functions. The
|
||||||
|
formatter is useful for inline printing of smaller data types similar to the
|
||||||
|
standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Custom Formatter Usage
|
||||||
|
|
||||||
|
The simplest way to make use of the spew custom formatter is to call one of the
|
||||||
|
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||||
|
functions have syntax you are most likely already familiar with:
|
||||||
|
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Println(myVar, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
See the Index for the full list convenience functions.
|
||||||
|
|
||||||
|
Sample Formatter Output
|
||||||
|
|
||||||
|
Double pointer to a uint8:
|
||||||
|
%v: <**>5
|
||||||
|
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||||
|
%#v: (**uint8)5
|
||||||
|
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||||
|
|
||||||
|
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||||
|
%v: <*>{1 <*><shown>}
|
||||||
|
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||||
|
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||||
|
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
||||||
|
|
||||||
|
See the Printf example for details on the setup of variables being shown
|
||||||
|
here.
|
||||||
|
|
||||||
|
Errors
|
||||||
|
|
||||||
|
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||||
|
detects them and handles them internally by printing the panic information
|
||||||
|
inline with the output. Since spew is intended to provide deep pretty printing
|
||||||
|
capabilities on structures, it intentionally does not return any errors.
|
||||||
|
*/
|
||||||
|
package spew
|
||||||
|
|
@ -0,0 +1,509 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// uint8Type is a reflect.Type representing a uint8. It is used to
|
||||||
|
// convert cgo types to uint8 slices for hexdumping.
|
||||||
|
uint8Type = reflect.TypeOf(uint8(0))
|
||||||
|
|
||||||
|
// cCharRE is a regular expression that matches a cgo char.
|
||||||
|
// It is used to detect character arrays to hexdump them.
|
||||||
|
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
|
||||||
|
|
||||||
|
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||||
|
// char. It is used to detect unsigned character arrays to hexdump
|
||||||
|
// them.
|
||||||
|
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
|
||||||
|
|
||||||
|
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||||
|
// It is used to detect uint8_t arrays to hexdump them.
|
||||||
|
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// dumpState contains information about the state of a dump operation.
|
||||||
|
type dumpState struct {
|
||||||
|
w io.Writer
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
ignoreNextIndent bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// indent performs indentation according to the depth level and cs.Indent
|
||||||
|
// option.
|
||||||
|
func (d *dumpState) indent() {
|
||||||
|
if d.ignoreNextIndent {
|
||||||
|
d.ignoreNextIndent = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range d.pointers {
|
||||||
|
if depth >= d.depth {
|
||||||
|
delete(d.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by dereferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d.pointers[addr] = d.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type information.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
d.w.Write([]byte(ve.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
|
||||||
|
// Display pointer information.
|
||||||
|
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
d.w.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(d.w, addr)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
switch {
|
||||||
|
case nilFound:
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound:
|
||||||
|
d.w.Write(circularBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
d.ignoreNextType = true
|
||||||
|
d.dump(ve)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
|
||||||
|
// reflection) arrays and slices are dumped in hexdump -C fashion.
|
||||||
|
func (d *dumpState) dumpSlice(v reflect.Value) {
|
||||||
|
// Determine whether this type should be hex dumped or not. Also,
|
||||||
|
// for types which should be hexdumped, try to use the underlying data
|
||||||
|
// first, then fall back to trying to convert them to a uint8 slice.
|
||||||
|
var buf []uint8
|
||||||
|
doConvert := false
|
||||||
|
doHexDump := false
|
||||||
|
numEntries := v.Len()
|
||||||
|
if numEntries > 0 {
|
||||||
|
vt := v.Index(0).Type()
|
||||||
|
vts := vt.String()
|
||||||
|
switch {
|
||||||
|
// C types that need to be converted.
|
||||||
|
case cCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUnsignedCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUint8tCharRE.MatchString(vts):
|
||||||
|
doConvert = true
|
||||||
|
|
||||||
|
// Try to use existing uint8 slices and fall back to converting
|
||||||
|
// and copying if that fails.
|
||||||
|
case vt.Kind() == reflect.Uint8:
|
||||||
|
// We need an addressable interface to convert the type
|
||||||
|
// to a byte slice. However, the reflect package won't
|
||||||
|
// give us an interface on certain things like
|
||||||
|
// unexported struct fields in order to enforce
|
||||||
|
// visibility rules. We use unsafe, when available, to
|
||||||
|
// bypass these restrictions since this package does not
|
||||||
|
// mutate the values.
|
||||||
|
vs := v
|
||||||
|
if !vs.CanInterface() || !vs.CanAddr() {
|
||||||
|
vs = unsafeReflectValue(vs)
|
||||||
|
}
|
||||||
|
if !UnsafeDisabled {
|
||||||
|
vs = vs.Slice(0, numEntries)
|
||||||
|
|
||||||
|
// Use the existing uint8 slice if it can be
|
||||||
|
// type asserted.
|
||||||
|
iface := vs.Interface()
|
||||||
|
if slice, ok := iface.([]uint8); ok {
|
||||||
|
buf = slice
|
||||||
|
doHexDump = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The underlying data needs to be converted if it can't
|
||||||
|
// be type asserted to a uint8 slice.
|
||||||
|
doConvert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy and convert the underlying type if needed.
|
||||||
|
if doConvert && vt.ConvertibleTo(uint8Type) {
|
||||||
|
// Convert and copy each element into a uint8 byte
|
||||||
|
// slice.
|
||||||
|
buf = make([]uint8, numEntries)
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
vv := v.Index(i)
|
||||||
|
buf[i] = uint8(vv.Convert(uint8Type).Uint())
|
||||||
|
}
|
||||||
|
doHexDump = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hexdump the entire slice as needed.
|
||||||
|
if doHexDump {
|
||||||
|
indent := strings.Repeat(d.cs.Indent, d.depth)
|
||||||
|
str := indent + hex.Dump(buf)
|
||||||
|
str = strings.Replace(str, "\n", "\n"+indent, -1)
|
||||||
|
str = strings.TrimRight(str, d.cs.Indent)
|
||||||
|
d.w.Write([]byte(str))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively call dump for each item.
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
d.dump(d.unpackValue(v.Index(i)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump is the main workhorse for dumping a value. It uses the passed reflect
|
||||||
|
// value to figure out what kind of object we are dealing with and formats it
|
||||||
|
// appropriately. It is a recursive function, however circular data structures
|
||||||
|
// are detected and handled properly.
|
||||||
|
func (d *dumpState) dump(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
d.w.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
d.indent()
|
||||||
|
d.dumpPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !d.ignoreNextType {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write([]byte(v.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.ignoreNextType = false
|
||||||
|
|
||||||
|
// Display length and capacity if the built-in len and cap functions
|
||||||
|
// work with the value's kind and the len/cap itself is non-zero.
|
||||||
|
valueLen, valueCap := 0, 0
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Array, reflect.Slice, reflect.Chan:
|
||||||
|
valueLen, valueCap = v.Len(), v.Cap()
|
||||||
|
case reflect.Map, reflect.String:
|
||||||
|
valueLen = v.Len()
|
||||||
|
}
|
||||||
|
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(lenEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueLen), 10)
|
||||||
|
}
|
||||||
|
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.w.Write(capEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueCap), 10)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods flag
|
||||||
|
// is enabled
|
||||||
|
if !d.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(d.cs, d.w, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(d.w, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(d.w, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(d.w, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(d.w, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(d.w, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(d.w, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(d.w, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.dumpSlice(v)
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
d.w.Write([]byte(strconv.Quote(v.String())))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if d.cs.SortKeys {
|
||||||
|
sortValues(keys, d.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
d.dump(d.unpackValue(key))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.MapIndex(key)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
numFields := v.NumField()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
d.indent()
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
d.w.Write([]byte(vtf.Name))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.Field(i)))
|
||||||
|
if i < (numFields - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(d.w, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(d.w, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it in case any new
|
||||||
|
// types are added.
|
||||||
|
default:
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fdump is a helper function to consolidate the logic from the various public
|
||||||
|
// methods which take varying writers and config states.
|
||||||
|
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
|
||||||
|
for _, arg := range a {
|
||||||
|
if arg == nil {
|
||||||
|
w.Write(interfaceBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
w.Write(newlineBytes)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d := dumpState{w: w, cs: cs}
|
||||||
|
d.pointers = make(map[uintptr]int)
|
||||||
|
d.dump(reflect.ValueOf(arg))
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(&Config, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(&Config, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by an exported package global,
|
||||||
|
spew.Config. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func Dump(a ...interface{}) {
|
||||||
|
fdump(&Config, os.Stdout, a...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,419 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// supportedFlags is a list of all the character flags supported by fmt package.
|
||||||
|
const supportedFlags = "0-+# "
|
||||||
|
|
||||||
|
// formatState implements the fmt.Formatter interface and contains information
|
||||||
|
// about the state of a formatting operation. The NewFormatter function can
|
||||||
|
// be used to get a new Formatter which can be used directly as arguments
|
||||||
|
// in standard fmt package printing calls.
|
||||||
|
type formatState struct {
|
||||||
|
value interface{}
|
||||||
|
fs fmt.State
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDefaultFormat recreates the original format string without precision
|
||||||
|
// and width information to pass in to fmt.Sprintf in the case of an
|
||||||
|
// unrecognized type. Unless new types are added to the language, this
|
||||||
|
// function won't ever be called.
|
||||||
|
func (f *formatState) buildDefaultFormat() (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune('v')
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructOrigFormat recreates the original format string including precision
|
||||||
|
// and width information to pass along to the standard fmt package. This allows
|
||||||
|
// automatic deferral of all format strings this package doesn't support.
|
||||||
|
func (f *formatState) constructOrigFormat(verb rune) (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width, ok := f.fs.Width(); ok {
|
||||||
|
buf.WriteString(strconv.Itoa(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
if precision, ok := f.fs.Precision(); ok {
|
||||||
|
buf.Write(precisionBytes)
|
||||||
|
buf.WriteString(strconv.Itoa(precision))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune(verb)
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible and
|
||||||
|
// ensures that types for values which have been unpacked from an interface
|
||||||
|
// are displayed when the show types flag is also set.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface {
|
||||||
|
f.ignoreNextType = false
|
||||||
|
if !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (f *formatState) formatPtr(v reflect.Value) {
|
||||||
|
// Display nil if top level pointer is nil.
|
||||||
|
showTypes := f.fs.Flag('#')
|
||||||
|
if v.IsNil() && (!showTypes || f.ignoreNextType) {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range f.pointers {
|
||||||
|
if depth >= f.depth {
|
||||||
|
delete(f.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to possibly show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by derferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f.pointers[addr] = f.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type or indirection level depending on flags.
|
||||||
|
if showTypes && !f.ignoreNextType {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
f.fs.Write([]byte(ve.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
} else {
|
||||||
|
if nilFound || cycleFound {
|
||||||
|
indirects += strings.Count(ve.Type().String(), "*")
|
||||||
|
}
|
||||||
|
f.fs.Write(openAngleBytes)
|
||||||
|
f.fs.Write([]byte(strings.Repeat("*", indirects)))
|
||||||
|
f.fs.Write(closeAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display pointer information depending on flags.
|
||||||
|
if f.fs.Flag('+') && (len(pointerChain) > 0) {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(f.fs, addr)
|
||||||
|
}
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
switch {
|
||||||
|
case nilFound:
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound:
|
||||||
|
f.fs.Write(circularShortBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(ve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format is the main workhorse for providing the Formatter interface. It
|
||||||
|
// uses the passed reflect value to figure out what kind of object we are
|
||||||
|
// dealing with and formats it appropriately. It is a recursive function,
|
||||||
|
// however circular data structures are detected and handled properly.
|
||||||
|
func (f *formatState) format(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
f.fs.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
f.formatPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !f.ignoreNextType && f.fs.Flag('#') {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write([]byte(v.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = false
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods
|
||||||
|
// flag is enabled.
|
||||||
|
if !f.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(f.cs, f.fs, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(f.fs, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(f.fs, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(f.fs, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(f.fs, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(f.fs, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(f.fs, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(f.fs, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
f.fs.Write(openBracketBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBracketBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
f.fs.Write([]byte(v.String()))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fs.Write(openMapBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if f.cs.SortKeys {
|
||||||
|
sortValues(keys, f.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(key))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.MapIndex(key)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeMapBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
numFields := v.NumField()
|
||||||
|
f.fs.Write(openBraceBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
if f.fs.Flag('+') || f.fs.Flag('#') {
|
||||||
|
f.fs.Write([]byte(vtf.Name))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
}
|
||||||
|
f.format(f.unpackValue(v.Field(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(f.fs, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(f.fs, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it if any get added.
|
||||||
|
default:
|
||||||
|
format := f.buildDefaultFormat()
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(f.fs, format, v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f.fs, format, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
|
||||||
|
// details.
|
||||||
|
func (f *formatState) Format(fs fmt.State, verb rune) {
|
||||||
|
f.fs = fs
|
||||||
|
|
||||||
|
// Use standard formatting for verbs that are not v.
|
||||||
|
if verb != 'v' {
|
||||||
|
format := f.constructOrigFormat(verb)
|
||||||
|
fmt.Fprintf(fs, format, f.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.value == nil {
|
||||||
|
if fs.Flag('#') {
|
||||||
|
fs.Write(interfaceBytes)
|
||||||
|
}
|
||||||
|
fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.format(reflect.ValueOf(f.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFormatter is a helper function to consolidate the logic from the various
|
||||||
|
// public methods which take varying config states.
|
||||||
|
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
|
||||||
|
fs := &formatState{value: v, cs: cs}
|
||||||
|
fs.pointers = make(map[uintptr]int)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
Printf, Println, or Fprintf.
|
||||||
|
*/
|
||||||
|
func NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(&Config, v)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the formatted string as a value that satisfies error. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a default spew Formatter interface.
|
||||||
|
func convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = NewFormatter(arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2014-2015 Docker, Inc.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More_considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||||
|
License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||||
|
License"). To the extent this Public License may be interpreted as a
|
||||||
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
|
such rights in consideration of benefits the Licensor receives from
|
||||||
|
making the Licensed Material available under these terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. BY-SA Compatible License means a license listed at
|
||||||
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
|
Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name
|
||||||
|
of a Creative Commons Public License. The License Elements of this
|
||||||
|
Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
k. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
l. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
m. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. Additional offer from the Licensor -- Adapted Material.
|
||||||
|
Every recipient of Adapted Material from You
|
||||||
|
automatically receives an offer from the Licensor to
|
||||||
|
exercise the Licensed Rights in the Adapted Material
|
||||||
|
under the conditions of the Adapter's License You apply.
|
||||||
|
|
||||||
|
c. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material (including in modified
|
||||||
|
form), You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
b. ShareAlike.
|
||||||
|
|
||||||
|
In addition to the conditions in Section 3(a), if You Share
|
||||||
|
Adapted Material You produce, the following conditions also apply.
|
||||||
|
|
||||||
|
1. The Adapter's License You apply must be a Creative Commons
|
||||||
|
license with the same License Elements, this version or
|
||||||
|
later, or a BY-SA Compatible License.
|
||||||
|
|
||||||
|
2. You must include the text of, or the URI or hyperlink to, the
|
||||||
|
Adapter's License You apply. You may satisfy this condition
|
||||||
|
in any reasonable manner based on the medium, means, and
|
||||||
|
context in which You Share Adapted Material.
|
||||||
|
|
||||||
|
3. You may not offer or impose any additional or different terms
|
||||||
|
or conditions on, or apply any Effective Technological
|
||||||
|
Measures to, Adapted Material that restrict exercise of the
|
||||||
|
rights granted under the Adapter's License You apply.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material,
|
||||||
|
|
||||||
|
including for purposes of Section 3(b); and
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public licenses.
|
||||||
|
Notwithstanding, Creative Commons may elect to apply one of its public
|
||||||
|
licenses to material it publishes and in those instances will be
|
||||||
|
considered the "Licensor." Except for the limited purpose of indicating
|
||||||
|
that material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the public
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
|
|
@ -0,0 +1,959 @@
|
||||||
|
package spdystream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/spdystream/spdy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidStreamId = errors.New("Invalid stream id")
|
||||||
|
ErrTimeout = errors.New("Timeout occurred")
|
||||||
|
ErrReset = errors.New("Stream reset")
|
||||||
|
ErrWriteClosedStream = errors.New("Write on closed stream")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FRAME_WORKERS = 5
|
||||||
|
QUEUE_SIZE = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamHandler func(stream *Stream)
|
||||||
|
|
||||||
|
type AuthHandler func(header http.Header, slot uint8, parent uint32) bool
|
||||||
|
|
||||||
|
type idleAwareFramer struct {
|
||||||
|
f *spdy.Framer
|
||||||
|
conn *Connection
|
||||||
|
writeLock sync.Mutex
|
||||||
|
resetChan chan struct{}
|
||||||
|
setTimeoutLock sync.Mutex
|
||||||
|
setTimeoutChan chan time.Duration
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIdleAwareFramer(framer *spdy.Framer) *idleAwareFramer {
|
||||||
|
iaf := &idleAwareFramer{
|
||||||
|
f: framer,
|
||||||
|
resetChan: make(chan struct{}, 2),
|
||||||
|
// setTimeoutChan needs to be buffered to avoid deadlocks when calling setIdleTimeout at about
|
||||||
|
// the same time the connection is being closed
|
||||||
|
setTimeoutChan: make(chan time.Duration, 1),
|
||||||
|
}
|
||||||
|
return iaf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idleAwareFramer) monitor() {
|
||||||
|
var (
|
||||||
|
timer *time.Timer
|
||||||
|
expired <-chan time.Time
|
||||||
|
resetChan = i.resetChan
|
||||||
|
setTimeoutChan = i.setTimeoutChan
|
||||||
|
)
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case timeout := <-i.setTimeoutChan:
|
||||||
|
i.timeout = timeout
|
||||||
|
if timeout == 0 {
|
||||||
|
if timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if timer == nil {
|
||||||
|
timer = time.NewTimer(timeout)
|
||||||
|
expired = timer.C
|
||||||
|
} else {
|
||||||
|
timer.Reset(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-resetChan:
|
||||||
|
if timer != nil && i.timeout > 0 {
|
||||||
|
timer.Reset(i.timeout)
|
||||||
|
}
|
||||||
|
case <-expired:
|
||||||
|
i.conn.streamCond.L.Lock()
|
||||||
|
streams := i.conn.streams
|
||||||
|
i.conn.streams = make(map[spdy.StreamId]*Stream)
|
||||||
|
i.conn.streamCond.Broadcast()
|
||||||
|
i.conn.streamCond.L.Unlock()
|
||||||
|
go func() {
|
||||||
|
for _, stream := range streams {
|
||||||
|
stream.resetStream()
|
||||||
|
}
|
||||||
|
i.conn.Close()
|
||||||
|
}()
|
||||||
|
case <-i.conn.closeChan:
|
||||||
|
if timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a goroutine to drain resetChan. This is needed because we've seen
|
||||||
|
// some unit tests with large numbers of goroutines get into a situation
|
||||||
|
// where resetChan fills up, at least 1 call to Write() is still trying to
|
||||||
|
// send to resetChan, the connection gets closed, and this case statement
|
||||||
|
// attempts to grab the write lock that Write() already has, causing a
|
||||||
|
// deadlock.
|
||||||
|
//
|
||||||
|
// See https://github.com/docker/spdystream/issues/49 for more details.
|
||||||
|
go func() {
|
||||||
|
for _ = range resetChan {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for _ = range setTimeoutChan {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
i.writeLock.Lock()
|
||||||
|
close(resetChan)
|
||||||
|
i.resetChan = nil
|
||||||
|
i.writeLock.Unlock()
|
||||||
|
|
||||||
|
i.setTimeoutLock.Lock()
|
||||||
|
close(i.setTimeoutChan)
|
||||||
|
i.setTimeoutChan = nil
|
||||||
|
i.setTimeoutLock.Unlock()
|
||||||
|
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain resetChan
|
||||||
|
for _ = range resetChan {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idleAwareFramer) WriteFrame(frame spdy.Frame) error {
|
||||||
|
i.writeLock.Lock()
|
||||||
|
defer i.writeLock.Unlock()
|
||||||
|
if i.resetChan == nil {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
err := i.f.WriteFrame(frame)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.resetChan <- struct{}{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idleAwareFramer) ReadFrame() (spdy.Frame, error) {
|
||||||
|
frame, err := i.f.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetChan should never be closed since it is only closed
|
||||||
|
// when the connection has closed its closeChan. This closure
|
||||||
|
// only occurs after all Reads have finished
|
||||||
|
// TODO (dmcgowan): refactor relationship into connection
|
||||||
|
i.resetChan <- struct{}{}
|
||||||
|
|
||||||
|
return frame, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *idleAwareFramer) setIdleTimeout(timeout time.Duration) {
|
||||||
|
i.setTimeoutLock.Lock()
|
||||||
|
defer i.setTimeoutLock.Unlock()
|
||||||
|
|
||||||
|
if i.setTimeoutChan == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setTimeoutChan <- timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
conn net.Conn
|
||||||
|
framer *idleAwareFramer
|
||||||
|
|
||||||
|
closeChan chan bool
|
||||||
|
goneAway bool
|
||||||
|
lastStreamChan chan<- *Stream
|
||||||
|
goAwayTimeout time.Duration
|
||||||
|
closeTimeout time.Duration
|
||||||
|
|
||||||
|
streamLock *sync.RWMutex
|
||||||
|
streamCond *sync.Cond
|
||||||
|
streams map[spdy.StreamId]*Stream
|
||||||
|
|
||||||
|
nextIdLock sync.Mutex
|
||||||
|
receiveIdLock sync.Mutex
|
||||||
|
nextStreamId spdy.StreamId
|
||||||
|
receivedStreamId spdy.StreamId
|
||||||
|
|
||||||
|
pingIdLock sync.Mutex
|
||||||
|
pingId uint32
|
||||||
|
pingChans map[uint32]chan error
|
||||||
|
|
||||||
|
shutdownLock sync.Mutex
|
||||||
|
shutdownChan chan error
|
||||||
|
hasShutdown bool
|
||||||
|
|
||||||
|
// for testing https://github.com/docker/spdystream/pull/56
|
||||||
|
dataFrameHandler func(*spdy.DataFrame) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnection creates a new spdy connection from an existing
|
||||||
|
// network connection.
|
||||||
|
func NewConnection(conn net.Conn, server bool) (*Connection, error) {
|
||||||
|
framer, framerErr := spdy.NewFramer(conn, conn)
|
||||||
|
if framerErr != nil {
|
||||||
|
return nil, framerErr
|
||||||
|
}
|
||||||
|
idleAwareFramer := newIdleAwareFramer(framer)
|
||||||
|
var sid spdy.StreamId
|
||||||
|
var rid spdy.StreamId
|
||||||
|
var pid uint32
|
||||||
|
if server {
|
||||||
|
sid = 2
|
||||||
|
rid = 1
|
||||||
|
pid = 2
|
||||||
|
} else {
|
||||||
|
sid = 1
|
||||||
|
rid = 2
|
||||||
|
pid = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
streamLock := new(sync.RWMutex)
|
||||||
|
streamCond := sync.NewCond(streamLock)
|
||||||
|
|
||||||
|
session := &Connection{
|
||||||
|
conn: conn,
|
||||||
|
framer: idleAwareFramer,
|
||||||
|
|
||||||
|
closeChan: make(chan bool),
|
||||||
|
goAwayTimeout: time.Duration(0),
|
||||||
|
closeTimeout: time.Duration(0),
|
||||||
|
|
||||||
|
streamLock: streamLock,
|
||||||
|
streamCond: streamCond,
|
||||||
|
streams: make(map[spdy.StreamId]*Stream),
|
||||||
|
nextStreamId: sid,
|
||||||
|
receivedStreamId: rid,
|
||||||
|
|
||||||
|
pingId: pid,
|
||||||
|
pingChans: make(map[uint32]chan error),
|
||||||
|
|
||||||
|
shutdownChan: make(chan error),
|
||||||
|
}
|
||||||
|
session.dataFrameHandler = session.handleDataFrame
|
||||||
|
idleAwareFramer.conn = session
|
||||||
|
go idleAwareFramer.monitor()
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping sends a ping frame across the connection and
|
||||||
|
// returns the response time
|
||||||
|
func (s *Connection) Ping() (time.Duration, error) {
|
||||||
|
pid := s.pingId
|
||||||
|
s.pingIdLock.Lock()
|
||||||
|
if s.pingId > 0x7ffffffe {
|
||||||
|
s.pingId = s.pingId - 0x7ffffffe
|
||||||
|
} else {
|
||||||
|
s.pingId = s.pingId + 2
|
||||||
|
}
|
||||||
|
s.pingIdLock.Unlock()
|
||||||
|
pingChan := make(chan error)
|
||||||
|
s.pingChans[pid] = pingChan
|
||||||
|
defer delete(s.pingChans, pid)
|
||||||
|
|
||||||
|
frame := &spdy.PingFrame{Id: pid}
|
||||||
|
startTime := time.Now()
|
||||||
|
writeErr := s.framer.WriteFrame(frame)
|
||||||
|
if writeErr != nil {
|
||||||
|
return time.Duration(0), writeErr
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
return time.Duration(0), errors.New("connection closed")
|
||||||
|
case err, ok := <-pingChan:
|
||||||
|
if ok && err != nil {
|
||||||
|
return time.Duration(0), err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return time.Now().Sub(startTime), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve handles frames sent from the server, including reply frames
|
||||||
|
// which are needed to fully initiate connections. Both clients and servers
|
||||||
|
// should call Serve in a separate goroutine before creating streams.
|
||||||
|
func (s *Connection) Serve(newHandler StreamHandler) {
|
||||||
|
// use a WaitGroup to wait for all frames to be drained after receiving
|
||||||
|
// go-away.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Parition queues to ensure stream frames are handled
|
||||||
|
// by the same worker, ensuring order is maintained
|
||||||
|
frameQueues := make([]*PriorityFrameQueue, FRAME_WORKERS)
|
||||||
|
for i := 0; i < FRAME_WORKERS; i++ {
|
||||||
|
frameQueues[i] = NewPriorityFrameQueue(QUEUE_SIZE)
|
||||||
|
|
||||||
|
// Ensure frame queue is drained when connection is closed
|
||||||
|
go func(frameQueue *PriorityFrameQueue) {
|
||||||
|
<-s.closeChan
|
||||||
|
frameQueue.Drain()
|
||||||
|
}(frameQueues[i])
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(frameQueue *PriorityFrameQueue) {
|
||||||
|
// let the WaitGroup know this worker is done
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
s.frameHandler(frameQueue, newHandler)
|
||||||
|
}(frameQueues[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
partitionRoundRobin int
|
||||||
|
goAwayFrame *spdy.GoAwayFrame
|
||||||
|
)
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
readFrame, err := s.framer.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
debugMessage("frame read error: %s", err)
|
||||||
|
} else {
|
||||||
|
debugMessage("(%p) EOF received", s)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var priority uint8
|
||||||
|
var partition int
|
||||||
|
switch frame := readFrame.(type) {
|
||||||
|
case *spdy.SynStreamFrame:
|
||||||
|
if s.checkStreamFrame(frame) {
|
||||||
|
priority = frame.Priority
|
||||||
|
partition = int(frame.StreamId % FRAME_WORKERS)
|
||||||
|
debugMessage("(%p) Add stream frame: %d ", s, frame.StreamId)
|
||||||
|
s.addStreamFrame(frame)
|
||||||
|
} else {
|
||||||
|
debugMessage("(%p) Rejected stream frame: %d ", s, frame.StreamId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case *spdy.SynReplyFrame:
|
||||||
|
priority = s.getStreamPriority(frame.StreamId)
|
||||||
|
partition = int(frame.StreamId % FRAME_WORKERS)
|
||||||
|
case *spdy.DataFrame:
|
||||||
|
priority = s.getStreamPriority(frame.StreamId)
|
||||||
|
partition = int(frame.StreamId % FRAME_WORKERS)
|
||||||
|
case *spdy.RstStreamFrame:
|
||||||
|
priority = s.getStreamPriority(frame.StreamId)
|
||||||
|
partition = int(frame.StreamId % FRAME_WORKERS)
|
||||||
|
case *spdy.HeadersFrame:
|
||||||
|
priority = s.getStreamPriority(frame.StreamId)
|
||||||
|
partition = int(frame.StreamId % FRAME_WORKERS)
|
||||||
|
case *spdy.PingFrame:
|
||||||
|
priority = 0
|
||||||
|
partition = partitionRoundRobin
|
||||||
|
partitionRoundRobin = (partitionRoundRobin + 1) % FRAME_WORKERS
|
||||||
|
case *spdy.GoAwayFrame:
|
||||||
|
// hold on to the go away frame and exit the loop
|
||||||
|
goAwayFrame = frame
|
||||||
|
break Loop
|
||||||
|
default:
|
||||||
|
priority = 7
|
||||||
|
partition = partitionRoundRobin
|
||||||
|
partitionRoundRobin = (partitionRoundRobin + 1) % FRAME_WORKERS
|
||||||
|
}
|
||||||
|
frameQueues[partition].Push(readFrame, priority)
|
||||||
|
}
|
||||||
|
close(s.closeChan)
|
||||||
|
|
||||||
|
// wait for all frame handler workers to indicate they've drained their queues
|
||||||
|
// before handling the go away frame
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if goAwayFrame != nil {
|
||||||
|
s.handleGoAwayFrame(goAwayFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now it's safe to close remote channels and empty s.streams
|
||||||
|
s.streamCond.L.Lock()
|
||||||
|
// notify streams that they're now closed, which will
|
||||||
|
// unblock any stream Read() calls
|
||||||
|
for _, stream := range s.streams {
|
||||||
|
stream.closeRemoteChannels()
|
||||||
|
}
|
||||||
|
s.streams = make(map[spdy.StreamId]*Stream)
|
||||||
|
s.streamCond.Broadcast()
|
||||||
|
s.streamCond.L.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) frameHandler(frameQueue *PriorityFrameQueue, newHandler StreamHandler) {
|
||||||
|
for {
|
||||||
|
popFrame := frameQueue.Pop()
|
||||||
|
if popFrame == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameErr error
|
||||||
|
switch frame := popFrame.(type) {
|
||||||
|
case *spdy.SynStreamFrame:
|
||||||
|
frameErr = s.handleStreamFrame(frame, newHandler)
|
||||||
|
case *spdy.SynReplyFrame:
|
||||||
|
frameErr = s.handleReplyFrame(frame)
|
||||||
|
case *spdy.DataFrame:
|
||||||
|
frameErr = s.dataFrameHandler(frame)
|
||||||
|
case *spdy.RstStreamFrame:
|
||||||
|
frameErr = s.handleResetFrame(frame)
|
||||||
|
case *spdy.HeadersFrame:
|
||||||
|
frameErr = s.handleHeaderFrame(frame)
|
||||||
|
case *spdy.PingFrame:
|
||||||
|
frameErr = s.handlePingFrame(frame)
|
||||||
|
case *spdy.GoAwayFrame:
|
||||||
|
frameErr = s.handleGoAwayFrame(frame)
|
||||||
|
default:
|
||||||
|
frameErr = fmt.Errorf("unhandled frame type: %T", frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameErr != nil {
|
||||||
|
debugMessage("frame handling error: %s", frameErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) getStreamPriority(streamId spdy.StreamId) uint8 {
|
||||||
|
stream, streamOk := s.getStream(streamId)
|
||||||
|
if !streamOk {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
return stream.priority
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) addStreamFrame(frame *spdy.SynStreamFrame) {
|
||||||
|
var parent *Stream
|
||||||
|
if frame.AssociatedToStreamId != spdy.StreamId(0) {
|
||||||
|
parent, _ = s.getStream(frame.AssociatedToStreamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := &Stream{
|
||||||
|
streamId: frame.StreamId,
|
||||||
|
parent: parent,
|
||||||
|
conn: s,
|
||||||
|
startChan: make(chan error),
|
||||||
|
headers: frame.Headers,
|
||||||
|
finished: (frame.CFHeader.Flags & spdy.ControlFlagUnidirectional) != 0x00,
|
||||||
|
replyCond: sync.NewCond(new(sync.Mutex)),
|
||||||
|
dataChan: make(chan []byte),
|
||||||
|
headerChan: make(chan http.Header),
|
||||||
|
closeChan: make(chan bool),
|
||||||
|
priority: frame.Priority,
|
||||||
|
}
|
||||||
|
if frame.CFHeader.Flags&spdy.ControlFlagFin != 0x00 {
|
||||||
|
stream.closeRemoteChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.addStream(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStreamFrame checks to see if a stream frame is allowed.
|
||||||
|
// If the stream is invalid, then a reset frame with protocol error
|
||||||
|
// will be returned.
|
||||||
|
func (s *Connection) checkStreamFrame(frame *spdy.SynStreamFrame) bool {
|
||||||
|
s.receiveIdLock.Lock()
|
||||||
|
defer s.receiveIdLock.Unlock()
|
||||||
|
if s.goneAway {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
validationErr := s.validateStreamId(frame.StreamId)
|
||||||
|
if validationErr != nil {
|
||||||
|
go func() {
|
||||||
|
resetErr := s.sendResetFrame(spdy.ProtocolError, frame.StreamId)
|
||||||
|
if resetErr != nil {
|
||||||
|
debugMessage("reset error: %s", resetErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleStreamFrame(frame *spdy.SynStreamFrame, newHandler StreamHandler) error {
|
||||||
|
stream, ok := s.getStream(frame.StreamId)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Missing stream: %d", frame.StreamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
newHandler(stream)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleReplyFrame(frame *spdy.SynReplyFrame) error {
|
||||||
|
debugMessage("(%p) Reply frame received for %d", s, frame.StreamId)
|
||||||
|
stream, streamOk := s.getStream(frame.StreamId)
|
||||||
|
if !streamOk {
|
||||||
|
debugMessage("Reply frame gone away for %d", frame.StreamId)
|
||||||
|
// Stream has already gone away
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if stream.replied {
|
||||||
|
// Stream has already received reply
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stream.replied = true
|
||||||
|
|
||||||
|
// TODO Check for error
|
||||||
|
if (frame.CFHeader.Flags & spdy.ControlFlagFin) != 0x00 {
|
||||||
|
s.remoteStreamFinish(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(stream.startChan)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleResetFrame(frame *spdy.RstStreamFrame) error {
|
||||||
|
stream, streamOk := s.getStream(frame.StreamId)
|
||||||
|
if !streamOk {
|
||||||
|
// Stream has already been removed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.removeStream(stream)
|
||||||
|
stream.closeRemoteChannels()
|
||||||
|
|
||||||
|
if !stream.replied {
|
||||||
|
stream.replied = true
|
||||||
|
stream.startChan <- ErrReset
|
||||||
|
close(stream.startChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.finishLock.Lock()
|
||||||
|
stream.finished = true
|
||||||
|
stream.finishLock.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleHeaderFrame(frame *spdy.HeadersFrame) error {
|
||||||
|
stream, streamOk := s.getStream(frame.StreamId)
|
||||||
|
if !streamOk {
|
||||||
|
// Stream has already gone away
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !stream.replied {
|
||||||
|
// No reply received...Protocol error?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO limit headers while not blocking (use buffered chan or goroutine?)
|
||||||
|
select {
|
||||||
|
case <-stream.closeChan:
|
||||||
|
return nil
|
||||||
|
case stream.headerChan <- frame.Headers:
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.CFHeader.Flags & spdy.ControlFlagFin) != 0x00 {
|
||||||
|
s.remoteStreamFinish(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleDataFrame(frame *spdy.DataFrame) error {
|
||||||
|
debugMessage("(%p) Data frame received for %d", s, frame.StreamId)
|
||||||
|
stream, streamOk := s.getStream(frame.StreamId)
|
||||||
|
if !streamOk {
|
||||||
|
debugMessage("(%p) Data frame gone away for %d", s, frame.StreamId)
|
||||||
|
// Stream has already gone away
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !stream.replied {
|
||||||
|
debugMessage("(%p) Data frame not replied %d", s, frame.StreamId)
|
||||||
|
// No reply received...Protocol error?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
debugMessage("(%p) (%d) Data frame handling", stream, stream.streamId)
|
||||||
|
if len(frame.Data) > 0 {
|
||||||
|
stream.dataLock.RLock()
|
||||||
|
select {
|
||||||
|
case <-stream.closeChan:
|
||||||
|
debugMessage("(%p) (%d) Data frame not sent (stream shut down)", stream, stream.streamId)
|
||||||
|
case stream.dataChan <- frame.Data:
|
||||||
|
debugMessage("(%p) (%d) Data frame sent", stream, stream.streamId)
|
||||||
|
}
|
||||||
|
stream.dataLock.RUnlock()
|
||||||
|
}
|
||||||
|
if (frame.Flags & spdy.DataFlagFin) != 0x00 {
|
||||||
|
s.remoteStreamFinish(stream)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handlePingFrame(frame *spdy.PingFrame) error {
|
||||||
|
if s.pingId&0x01 != frame.Id&0x01 {
|
||||||
|
return s.framer.WriteFrame(frame)
|
||||||
|
}
|
||||||
|
pingChan, pingOk := s.pingChans[frame.Id]
|
||||||
|
if pingOk {
|
||||||
|
close(pingChan)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) handleGoAwayFrame(frame *spdy.GoAwayFrame) error {
|
||||||
|
debugMessage("(%p) Go away received", s)
|
||||||
|
s.receiveIdLock.Lock()
|
||||||
|
if s.goneAway {
|
||||||
|
s.receiveIdLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.goneAway = true
|
||||||
|
s.receiveIdLock.Unlock()
|
||||||
|
|
||||||
|
if s.lastStreamChan != nil {
|
||||||
|
stream, _ := s.getStream(frame.LastGoodStreamId)
|
||||||
|
go func() {
|
||||||
|
s.lastStreamChan <- stream
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not block frame handler waiting for closure
|
||||||
|
go s.shutdown(s.goAwayTimeout)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) remoteStreamFinish(stream *Stream) {
|
||||||
|
stream.closeRemoteChannels()
|
||||||
|
|
||||||
|
stream.finishLock.Lock()
|
||||||
|
if stream.finished {
|
||||||
|
// Stream is fully closed, cleanup
|
||||||
|
s.removeStream(stream)
|
||||||
|
}
|
||||||
|
stream.finishLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStream creates a new spdy stream using the parameters for
|
||||||
|
// creating the stream frame. The stream frame will be sent upon
|
||||||
|
// calling this function, however this function does not wait for
|
||||||
|
// the reply frame. If waiting for the reply is desired, use
|
||||||
|
// the stream Wait or WaitTimeout function on the stream returned
|
||||||
|
// by this function.
|
||||||
|
func (s *Connection) CreateStream(headers http.Header, parent *Stream, fin bool) (*Stream, error) {
|
||||||
|
// MUST synchronize stream creation (all the way to writing the frame)
|
||||||
|
// as stream IDs **MUST** increase monotonically.
|
||||||
|
s.nextIdLock.Lock()
|
||||||
|
defer s.nextIdLock.Unlock()
|
||||||
|
|
||||||
|
streamId := s.getNextStreamId()
|
||||||
|
if streamId == 0 {
|
||||||
|
return nil, fmt.Errorf("Unable to get new stream id")
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := &Stream{
|
||||||
|
streamId: streamId,
|
||||||
|
parent: parent,
|
||||||
|
conn: s,
|
||||||
|
startChan: make(chan error),
|
||||||
|
headers: headers,
|
||||||
|
dataChan: make(chan []byte),
|
||||||
|
headerChan: make(chan http.Header),
|
||||||
|
closeChan: make(chan bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
debugMessage("(%p) (%p) Create stream", s, stream)
|
||||||
|
|
||||||
|
s.addStream(stream)
|
||||||
|
|
||||||
|
return stream, s.sendStream(stream, fin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) shutdown(closeTimeout time.Duration) {
|
||||||
|
// TODO Ensure this isn't called multiple times
|
||||||
|
s.shutdownLock.Lock()
|
||||||
|
if s.hasShutdown {
|
||||||
|
s.shutdownLock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.hasShutdown = true
|
||||||
|
s.shutdownLock.Unlock()
|
||||||
|
|
||||||
|
var timeout <-chan time.Time
|
||||||
|
if closeTimeout > time.Duration(0) {
|
||||||
|
timeout = time.After(closeTimeout)
|
||||||
|
}
|
||||||
|
streamsClosed := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.streamCond.L.Lock()
|
||||||
|
for len(s.streams) > 0 {
|
||||||
|
debugMessage("Streams opened: %d, %#v", len(s.streams), s.streams)
|
||||||
|
s.streamCond.Wait()
|
||||||
|
}
|
||||||
|
s.streamCond.L.Unlock()
|
||||||
|
close(streamsClosed)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case <-streamsClosed:
|
||||||
|
// No active streams, close should be safe
|
||||||
|
err = s.conn.Close()
|
||||||
|
case <-timeout:
|
||||||
|
// Force ungraceful close
|
||||||
|
err = s.conn.Close()
|
||||||
|
// Wait for cleanup to clear active streams
|
||||||
|
<-streamsClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
duration := 10 * time.Minute
|
||||||
|
time.AfterFunc(duration, func() {
|
||||||
|
select {
|
||||||
|
case err, ok := <-s.shutdownChan:
|
||||||
|
if ok {
|
||||||
|
debugMessage("Unhandled close error after %s: %s", duration, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.shutdownChan <- err
|
||||||
|
}
|
||||||
|
close(s.shutdownChan)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes spdy connection by sending GoAway frame and initiating shutdown
|
||||||
|
func (s *Connection) Close() error {
|
||||||
|
s.receiveIdLock.Lock()
|
||||||
|
if s.goneAway {
|
||||||
|
s.receiveIdLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.goneAway = true
|
||||||
|
s.receiveIdLock.Unlock()
|
||||||
|
|
||||||
|
var lastStreamId spdy.StreamId
|
||||||
|
if s.receivedStreamId > 2 {
|
||||||
|
lastStreamId = s.receivedStreamId - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
goAwayFrame := &spdy.GoAwayFrame{
|
||||||
|
LastGoodStreamId: lastStreamId,
|
||||||
|
Status: spdy.GoAwayOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.framer.WriteFrame(goAwayFrame)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.shutdown(s.closeTimeout)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseWait closes the connection and waits for shutdown
|
||||||
|
// to finish. Note the underlying network Connection
|
||||||
|
// is not closed until the end of shutdown.
|
||||||
|
func (s *Connection) CloseWait() error {
|
||||||
|
closeErr := s.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
shutdownErr, ok := <-s.shutdownChan
|
||||||
|
if ok {
|
||||||
|
return shutdownErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the connection to finish shutdown or for
|
||||||
|
// the wait timeout duration to expire. This needs to be
|
||||||
|
// called either after Close has been called or the GOAWAYFRAME
|
||||||
|
// has been received. If the wait timeout is 0, this function
|
||||||
|
// will block until shutdown finishes. If wait is never called
|
||||||
|
// and a shutdown error occurs, that error will be logged as an
|
||||||
|
// unhandled error.
|
||||||
|
func (s *Connection) Wait(waitTimeout time.Duration) error {
|
||||||
|
var timeout <-chan time.Time
|
||||||
|
if waitTimeout > time.Duration(0) {
|
||||||
|
timeout = time.After(waitTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err, ok := <-s.shutdownChan:
|
||||||
|
if ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
return ErrTimeout
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyClose registers a channel to be called when the remote
|
||||||
|
// peer inidicates connection closure. The last stream to be
|
||||||
|
// received by the remote will be sent on the channel. The notify
|
||||||
|
// timeout will determine the duration between go away received
|
||||||
|
// and the connection being closed.
|
||||||
|
func (s *Connection) NotifyClose(c chan<- *Stream, timeout time.Duration) {
|
||||||
|
s.goAwayTimeout = timeout
|
||||||
|
s.lastStreamChan = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCloseTimeout sets the amount of time close will wait for
|
||||||
|
// streams to finish before terminating the underlying network
|
||||||
|
// connection. Setting the timeout to 0 will cause close to
|
||||||
|
// wait forever, which is the default.
|
||||||
|
func (s *Connection) SetCloseTimeout(timeout time.Duration) {
|
||||||
|
s.closeTimeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIdleTimeout sets the amount of time the connection may sit idle before
|
||||||
|
// it is forcefully terminated.
|
||||||
|
func (s *Connection) SetIdleTimeout(timeout time.Duration) {
|
||||||
|
s.framer.setIdleTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) sendHeaders(headers http.Header, stream *Stream, fin bool) error {
|
||||||
|
var flags spdy.ControlFlags
|
||||||
|
if fin {
|
||||||
|
flags = spdy.ControlFlagFin
|
||||||
|
}
|
||||||
|
|
||||||
|
headerFrame := &spdy.HeadersFrame{
|
||||||
|
StreamId: stream.streamId,
|
||||||
|
Headers: headers,
|
||||||
|
CFHeader: spdy.ControlFrameHeader{Flags: flags},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.framer.WriteFrame(headerFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) sendReply(headers http.Header, stream *Stream, fin bool) error {
|
||||||
|
var flags spdy.ControlFlags
|
||||||
|
if fin {
|
||||||
|
flags = spdy.ControlFlagFin
|
||||||
|
}
|
||||||
|
|
||||||
|
replyFrame := &spdy.SynReplyFrame{
|
||||||
|
StreamId: stream.streamId,
|
||||||
|
Headers: headers,
|
||||||
|
CFHeader: spdy.ControlFrameHeader{Flags: flags},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.framer.WriteFrame(replyFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) sendResetFrame(status spdy.RstStreamStatus, streamId spdy.StreamId) error {
|
||||||
|
resetFrame := &spdy.RstStreamFrame{
|
||||||
|
StreamId: streamId,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.framer.WriteFrame(resetFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) sendReset(status spdy.RstStreamStatus, stream *Stream) error {
|
||||||
|
return s.sendResetFrame(status, stream.streamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) sendStream(stream *Stream, fin bool) error {
|
||||||
|
var flags spdy.ControlFlags
|
||||||
|
if fin {
|
||||||
|
flags = spdy.ControlFlagFin
|
||||||
|
stream.finished = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentId spdy.StreamId
|
||||||
|
if stream.parent != nil {
|
||||||
|
parentId = stream.parent.streamId
|
||||||
|
}
|
||||||
|
|
||||||
|
streamFrame := &spdy.SynStreamFrame{
|
||||||
|
StreamId: spdy.StreamId(stream.streamId),
|
||||||
|
AssociatedToStreamId: spdy.StreamId(parentId),
|
||||||
|
Headers: stream.headers,
|
||||||
|
CFHeader: spdy.ControlFrameHeader{Flags: flags},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.framer.WriteFrame(streamFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNextStreamId returns the next sequential id
|
||||||
|
// every call should produce a unique value or an error
|
||||||
|
func (s *Connection) getNextStreamId() spdy.StreamId {
|
||||||
|
sid := s.nextStreamId
|
||||||
|
if sid > 0x7fffffff {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
s.nextStreamId = s.nextStreamId + 2
|
||||||
|
return sid
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekNextStreamId returns the next sequential id and keeps the next id untouched
|
||||||
|
func (s *Connection) PeekNextStreamId() spdy.StreamId {
|
||||||
|
sid := s.nextStreamId
|
||||||
|
return sid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) validateStreamId(rid spdy.StreamId) error {
|
||||||
|
if rid > 0x7fffffff || rid < s.receivedStreamId {
|
||||||
|
return ErrInvalidStreamId
|
||||||
|
}
|
||||||
|
s.receivedStreamId = rid + 2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) addStream(stream *Stream) {
|
||||||
|
s.streamCond.L.Lock()
|
||||||
|
s.streams[stream.streamId] = stream
|
||||||
|
debugMessage("(%p) (%p) Stream added, broadcasting: %d", s, stream, stream.streamId)
|
||||||
|
s.streamCond.Broadcast()
|
||||||
|
s.streamCond.L.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) removeStream(stream *Stream) {
|
||||||
|
s.streamCond.L.Lock()
|
||||||
|
delete(s.streams, stream.streamId)
|
||||||
|
debugMessage("(%p) (%p) Stream removed, broadcasting: %d", s, stream, stream.streamId)
|
||||||
|
s.streamCond.Broadcast()
|
||||||
|
s.streamCond.L.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) getStream(streamId spdy.StreamId) (stream *Stream, ok bool) {
|
||||||
|
s.streamLock.RLock()
|
||||||
|
stream, ok = s.streams[streamId]
|
||||||
|
s.streamLock.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindStream looks up the given stream id and either waits for the
|
||||||
|
// stream to be found or returns nil if the stream id is no longer
|
||||||
|
// valid.
|
||||||
|
func (s *Connection) FindStream(streamId uint32) *Stream {
|
||||||
|
var stream *Stream
|
||||||
|
var ok bool
|
||||||
|
s.streamCond.L.Lock()
|
||||||
|
stream, ok = s.streams[spdy.StreamId(streamId)]
|
||||||
|
debugMessage("(%p) Found stream %d? %t", s, spdy.StreamId(streamId), ok)
|
||||||
|
for !ok && streamId >= uint32(s.receivedStreamId) {
|
||||||
|
s.streamCond.Wait()
|
||||||
|
stream, ok = s.streams[spdy.StreamId(streamId)]
|
||||||
|
}
|
||||||
|
s.streamCond.L.Unlock()
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connection) CloseChan() <-chan bool {
|
||||||
|
return s.closeChan
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package spdystream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MirrorStreamHandler mirrors all streams.
|
||||||
|
func MirrorStreamHandler(stream *Stream) {
|
||||||
|
replyErr := stream.SendReply(http.Header{}, false)
|
||||||
|
if replyErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
io.Copy(stream, stream)
|
||||||
|
stream.Close()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
header, receiveErr := stream.ReceiveHeader()
|
||||||
|
if receiveErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendErr := stream.SendHeader(header, false)
|
||||||
|
if sendErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoopStreamHandler does nothing when stream connects.
|
||||||
|
func NoOpStreamHandler(stream *Stream) {
|
||||||
|
stream.SendReply(http.Header{}, false)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package spdystream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/heap"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/spdystream/spdy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prioritizedFrame struct {
|
||||||
|
frame spdy.Frame
|
||||||
|
priority uint8
|
||||||
|
insertId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type frameQueue []*prioritizedFrame
|
||||||
|
|
||||||
|
func (fq frameQueue) Len() int {
|
||||||
|
return len(fq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fq frameQueue) Less(i, j int) bool {
|
||||||
|
if fq[i].priority == fq[j].priority {
|
||||||
|
return fq[i].insertId < fq[j].insertId
|
||||||
|
}
|
||||||
|
return fq[i].priority < fq[j].priority
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fq frameQueue) Swap(i, j int) {
|
||||||
|
fq[i], fq[j] = fq[j], fq[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fq *frameQueue) Push(x interface{}) {
|
||||||
|
*fq = append(*fq, x.(*prioritizedFrame))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fq *frameQueue) Pop() interface{} {
|
||||||
|
old := *fq
|
||||||
|
n := len(old)
|
||||||
|
*fq = old[0 : n-1]
|
||||||
|
return old[n-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriorityFrameQueue struct {
|
||||||
|
queue *frameQueue
|
||||||
|
c *sync.Cond
|
||||||
|
size int
|
||||||
|
nextInsertId uint64
|
||||||
|
drain bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPriorityFrameQueue(size int) *PriorityFrameQueue {
|
||||||
|
queue := make(frameQueue, 0, size)
|
||||||
|
heap.Init(&queue)
|
||||||
|
|
||||||
|
return &PriorityFrameQueue{
|
||||||
|
queue: &queue,
|
||||||
|
size: size,
|
||||||
|
c: sync.NewCond(&sync.Mutex{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *PriorityFrameQueue) Push(frame spdy.Frame, priority uint8) {
|
||||||
|
q.c.L.Lock()
|
||||||
|
defer q.c.L.Unlock()
|
||||||
|
for q.queue.Len() >= q.size {
|
||||||
|
q.c.Wait()
|
||||||
|
}
|
||||||
|
pFrame := &prioritizedFrame{
|
||||||
|
frame: frame,
|
||||||
|
priority: priority,
|
||||||
|
insertId: q.nextInsertId,
|
||||||
|
}
|
||||||
|
q.nextInsertId = q.nextInsertId + 1
|
||||||
|
heap.Push(q.queue, pFrame)
|
||||||
|
q.c.Signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *PriorityFrameQueue) Pop() spdy.Frame {
|
||||||
|
q.c.L.Lock()
|
||||||
|
defer q.c.L.Unlock()
|
||||||
|
for q.queue.Len() == 0 {
|
||||||
|
if q.drain {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
q.c.Wait()
|
||||||
|
}
|
||||||
|
frame := heap.Pop(q.queue).(*prioritizedFrame).frame
|
||||||
|
q.c.Signal()
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *PriorityFrameQueue) Drain() {
|
||||||
|
q.c.L.Lock()
|
||||||
|
defer q.c.L.Unlock()
|
||||||
|
q.drain = true
|
||||||
|
q.c.Broadcast()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package spdy
|
||||||
|
|
||||||
|
// headerDictionary is the dictionary sent to the zlib compressor/decompressor.
|
||||||
|
var headerDictionary = []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68,
|
||||||
|
0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70,
|
||||||
|
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70,
|
||||||
|
0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65,
|
||||||
|
0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05,
|
||||||
|
0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00,
|
||||||
|
0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00,
|
||||||
|
0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70,
|
||||||
|
0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
|
||||||
|
0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63,
|
||||||
|
0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f,
|
||||||
|
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f,
|
||||||
|
0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c,
|
||||||
|
0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00,
|
||||||
|
0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70,
|
||||||
|
0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00,
|
||||||
|
0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
|
||||||
|
0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68,
|
||||||
|
0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
|
||||||
|
0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63,
|
||||||
|
0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72,
|
||||||
|
0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f,
|
||||||
|
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
|
0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74,
|
||||||
|
0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65,
|
||||||
|
0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74,
|
||||||
|
0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f,
|
||||||
|
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10,
|
||||||
|
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d,
|
||||||
|
0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65,
|
||||||
|
0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74,
|
||||||
|
0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67,
|
||||||
|
0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f,
|
||||||
|
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f,
|
||||||
|
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00,
|
||||||
|
0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
|
||||||
|
0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00,
|
||||||
|
0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74,
|
||||||
|
0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00,
|
||||||
|
0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
|
||||||
|
0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00,
|
||||||
|
0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00,
|
||||||
|
0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00,
|
||||||
|
0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74,
|
||||||
|
0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69,
|
||||||
|
0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66,
|
||||||
|
0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68,
|
||||||
|
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69,
|
||||||
|
0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00,
|
||||||
|
0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f,
|
||||||
|
0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73,
|
||||||
|
0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d,
|
||||||
|
0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d,
|
||||||
|
0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00,
|
||||||
|
0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67,
|
||||||
|
0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d,
|
||||||
|
0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69,
|
||||||
|
0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65,
|
||||||
|
0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74,
|
||||||
|
0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65,
|
||||||
|
0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63,
|
||||||
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00,
|
||||||
|
0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72,
|
||||||
|
0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00,
|
||||||
|
0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00,
|
||||||
|
0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79,
|
||||||
|
0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
|
||||||
|
0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00,
|
||||||
|
0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61,
|
||||||
|
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05,
|
||||||
|
0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00,
|
||||||
|
0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72,
|
||||||
|
0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72,
|
||||||
|
0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00,
|
||||||
|
0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||||
|
0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00,
|
||||||
|
0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c,
|
||||||
|
0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72,
|
||||||
|
0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65,
|
||||||
|
0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00,
|
||||||
|
0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61,
|
||||||
|
0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73,
|
||||||
|
0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||||
|
0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00,
|
||||||
|
0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69,
|
||||||
|
0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77,
|
||||||
|
0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e,
|
||||||
|
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00,
|
||||||
|
0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00,
|
||||||
|
0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
|
||||||
|
0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30,
|
||||||
|
0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76,
|
||||||
|
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00,
|
||||||
|
0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31,
|
||||||
|
0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72,
|
||||||
|
0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62,
|
||||||
|
0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73,
|
||||||
|
0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69,
|
||||||
|
0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65,
|
||||||
|
0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00,
|
||||||
|
0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69,
|
||||||
|
0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32,
|
||||||
|
0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35,
|
||||||
|
0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30,
|
||||||
|
0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33,
|
||||||
|
0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37,
|
||||||
|
0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30,
|
||||||
|
0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34,
|
||||||
|
0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31,
|
||||||
|
0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31,
|
||||||
|
0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34,
|
||||||
|
0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34,
|
||||||
|
0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e,
|
||||||
|
0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f,
|
||||||
|
0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65,
|
||||||
|
0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20,
|
||||||
|
0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65,
|
||||||
|
0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f,
|
||||||
|
0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d,
|
||||||
|
0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34,
|
||||||
|
0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30,
|
||||||
|
0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68,
|
||||||
|
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30,
|
||||||
|
0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64,
|
||||||
|
0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e,
|
||||||
|
0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64,
|
||||||
|
0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65,
|
||||||
|
0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72,
|
||||||
|
0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f,
|
||||||
|
0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74,
|
||||||
|
0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65,
|
||||||
|
0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20,
|
||||||
|
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20,
|
||||||
|
0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61,
|
||||||
|
0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46,
|
||||||
|
0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41,
|
||||||
|
0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a,
|
||||||
|
0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41,
|
||||||
|
0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20,
|
||||||
|
0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20,
|
||||||
|
0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30,
|
||||||
|
0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e,
|
||||||
|
0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57,
|
||||||
|
0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c,
|
||||||
|
0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61,
|
||||||
|
0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20,
|
||||||
|
0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b,
|
||||||
|
0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f,
|
||||||
|
0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61,
|
||||||
|
0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69,
|
||||||
|
0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67,
|
||||||
|
0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67,
|
||||||
|
0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69,
|
||||||
|
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78,
|
||||||
|
0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69,
|
||||||
|
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78,
|
||||||
|
0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c,
|
||||||
|
0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c,
|
||||||
|
0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74,
|
||||||
|
0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72,
|
||||||
|
0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c,
|
||||||
|
0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
|
||||||
|
0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65,
|
||||||
|
0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65,
|
||||||
|
0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64,
|
||||||
|
0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
|
||||||
|
0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63,
|
||||||
|
0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69,
|
||||||
|
0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d,
|
||||||
|
0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a,
|
||||||
|
0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package spdy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (frame *SynStreamFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
return f.readSynStreamFrame(h, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *SynReplyFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
return f.readSynReplyFrame(h, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *RstStreamFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.Status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frame.Status == 0 {
|
||||||
|
return &Error{InvalidControlFrame, frame.StreamId}
|
||||||
|
}
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *SettingsFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
var numSettings uint32
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &numSettings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame.FlagIdValues = make([]SettingsFlagIdValue, numSettings)
|
||||||
|
for i := uint32(0); i < numSettings; i++ {
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.FlagIdValues[i].Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame.FlagIdValues[i].Flag = SettingsFlag((frame.FlagIdValues[i].Id & 0xff000000) >> 24)
|
||||||
|
frame.FlagIdValues[i].Id &= 0xffffff
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.FlagIdValues[i].Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *PingFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frame.Id == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
if frame.CFHeader.Flags != 0 {
|
||||||
|
return &Error{InvalidControlFrame, StreamId(frame.Id)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *GoAwayFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.LastGoodStreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frame.CFHeader.Flags != 0 {
|
||||||
|
return &Error{InvalidControlFrame, frame.LastGoodStreamId}
|
||||||
|
}
|
||||||
|
if frame.CFHeader.length != 8 {
|
||||||
|
return &Error{InvalidControlFrame, frame.LastGoodStreamId}
|
||||||
|
}
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.Status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *HeadersFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
return f.readHeadersFrame(h, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *WindowUpdateFrame) read(h ControlFrameHeader, f *Framer) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frame.CFHeader.Flags != 0 {
|
||||||
|
return &Error{InvalidControlFrame, frame.StreamId}
|
||||||
|
}
|
||||||
|
if frame.CFHeader.length != 8 {
|
||||||
|
return &Error{InvalidControlFrame, frame.StreamId}
|
||||||
|
}
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &frame.DeltaWindowSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newControlFrame(frameType ControlFrameType) (controlFrame, error) {
|
||||||
|
ctor, ok := cframeCtor[frameType]
|
||||||
|
if !ok {
|
||||||
|
return nil, &Error{Err: InvalidControlFrame}
|
||||||
|
}
|
||||||
|
return ctor(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cframeCtor = map[ControlFrameType]func() controlFrame{
|
||||||
|
TypeSynStream: func() controlFrame { return new(SynStreamFrame) },
|
||||||
|
TypeSynReply: func() controlFrame { return new(SynReplyFrame) },
|
||||||
|
TypeRstStream: func() controlFrame { return new(RstStreamFrame) },
|
||||||
|
TypeSettings: func() controlFrame { return new(SettingsFrame) },
|
||||||
|
TypePing: func() controlFrame { return new(PingFrame) },
|
||||||
|
TypeGoAway: func() controlFrame { return new(GoAwayFrame) },
|
||||||
|
TypeHeaders: func() controlFrame { return new(HeadersFrame) },
|
||||||
|
TypeWindowUpdate: func() controlFrame { return new(WindowUpdateFrame) },
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) uncorkHeaderDecompressor(payloadSize int64) error {
|
||||||
|
if f.headerDecompressor != nil {
|
||||||
|
f.headerReader.N = payloadSize
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f.headerReader = io.LimitedReader{R: f.r, N: payloadSize}
|
||||||
|
decompressor, err := zlib.NewReaderDict(&f.headerReader, []byte(headerDictionary))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.headerDecompressor = decompressor
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrame reads SPDY encoded data and returns a decompressed Frame.
|
||||||
|
func (f *Framer) ReadFrame() (Frame, error) {
|
||||||
|
var firstWord uint32
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &firstWord); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if firstWord&0x80000000 != 0 {
|
||||||
|
frameType := ControlFrameType(firstWord & 0xffff)
|
||||||
|
version := uint16(firstWord >> 16 & 0x7fff)
|
||||||
|
return f.parseControlFrame(version, frameType)
|
||||||
|
}
|
||||||
|
return f.parseDataFrame(StreamId(firstWord & 0x7fffffff))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) parseControlFrame(version uint16, frameType ControlFrameType) (Frame, error) {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
flags := ControlFlags((length & 0xff000000) >> 24)
|
||||||
|
length &= 0xffffff
|
||||||
|
header := ControlFrameHeader{version, frameType, flags, length}
|
||||||
|
cframe, err := newControlFrame(frameType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = cframe.read(header, f); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cframe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeaderValueBlock(r io.Reader, streamId StreamId) (http.Header, error) {
|
||||||
|
var numHeaders uint32
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &numHeaders); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var e error
|
||||||
|
h := make(http.Header, int(numHeaders))
|
||||||
|
for i := 0; i < int(numHeaders); i++ {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nameBytes := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, nameBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := string(nameBytes)
|
||||||
|
if name != strings.ToLower(name) {
|
||||||
|
e = &Error{UnlowercasedHeaderName, streamId}
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
}
|
||||||
|
if h[name] != nil {
|
||||||
|
e = &Error{DuplicateHeaders, streamId}
|
||||||
|
}
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueList := strings.Split(string(value), headerValueSeparator)
|
||||||
|
for _, v := range valueList {
|
||||||
|
h.Add(name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return h, e
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) readSynStreamFrame(h ControlFrameHeader, frame *SynStreamFrame) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
var err error
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.AssociatedToStreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.Priority); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame.Priority >>= 5
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.Slot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader := f.r
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
err := f.uncorkHeaderDecompressor(int64(h.length - 10))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader = f.headerDecompressor
|
||||||
|
}
|
||||||
|
frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
|
||||||
|
if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
|
||||||
|
err = &Error{WrongCompressedPayloadSize, 0}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for h := range frame.Headers {
|
||||||
|
if invalidReqHeaders[h] {
|
||||||
|
return &Error{InvalidHeaderPresent, frame.StreamId}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) readSynReplyFrame(h ControlFrameHeader, frame *SynReplyFrame) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
var err error
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader := f.r
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
err := f.uncorkHeaderDecompressor(int64(h.length - 4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader = f.headerDecompressor
|
||||||
|
}
|
||||||
|
frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
|
||||||
|
if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
|
||||||
|
err = &Error{WrongCompressedPayloadSize, 0}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for h := range frame.Headers {
|
||||||
|
if invalidRespHeaders[h] {
|
||||||
|
return &Error{InvalidHeaderPresent, frame.StreamId}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) readHeadersFrame(h ControlFrameHeader, frame *HeadersFrame) error {
|
||||||
|
frame.CFHeader = h
|
||||||
|
var err error
|
||||||
|
if err = binary.Read(f.r, binary.BigEndian, &frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader := f.r
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
err := f.uncorkHeaderDecompressor(int64(h.length - 4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader = f.headerDecompressor
|
||||||
|
}
|
||||||
|
frame.Headers, err = parseHeaderValueBlock(reader, frame.StreamId)
|
||||||
|
if !f.headerCompressionDisabled && (err == io.EOF && f.headerReader.N == 0 || f.headerReader.N != 0) {
|
||||||
|
err = &Error{WrongCompressedPayloadSize, 0}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var invalidHeaders map[string]bool
|
||||||
|
if frame.StreamId%2 == 0 {
|
||||||
|
invalidHeaders = invalidReqHeaders
|
||||||
|
} else {
|
||||||
|
invalidHeaders = invalidRespHeaders
|
||||||
|
}
|
||||||
|
for h := range frame.Headers {
|
||||||
|
if invalidHeaders[h] {
|
||||||
|
return &Error{InvalidHeaderPresent, frame.StreamId}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) parseDataFrame(streamId StreamId) (*DataFrame, error) {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(f.r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var frame DataFrame
|
||||||
|
frame.StreamId = streamId
|
||||||
|
frame.Flags = DataFlags(length >> 24)
|
||||||
|
length &= 0xffffff
|
||||||
|
frame.Data = make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(f.r, frame.Data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return nil, &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
return &frame, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package spdy implements the SPDY protocol (currently SPDY/3), described in
|
||||||
|
// http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3.
|
||||||
|
package spdy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is the protocol version number that this package implements.
|
||||||
|
const Version = 3
|
||||||
|
|
||||||
|
// ControlFrameType stores the type field in a control frame header.
|
||||||
|
type ControlFrameType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeSynStream ControlFrameType = 0x0001
|
||||||
|
TypeSynReply = 0x0002
|
||||||
|
TypeRstStream = 0x0003
|
||||||
|
TypeSettings = 0x0004
|
||||||
|
TypePing = 0x0006
|
||||||
|
TypeGoAway = 0x0007
|
||||||
|
TypeHeaders = 0x0008
|
||||||
|
TypeWindowUpdate = 0x0009
|
||||||
|
)
|
||||||
|
|
||||||
|
// ControlFlags are the flags that can be set on a control frame.
|
||||||
|
type ControlFlags uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ControlFlagFin ControlFlags = 0x01
|
||||||
|
ControlFlagUnidirectional = 0x02
|
||||||
|
ControlFlagSettingsClearSettings = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataFlags are the flags that can be set on a data frame.
|
||||||
|
type DataFlags uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
DataFlagFin DataFlags = 0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxDataLength is the maximum number of bytes that can be stored in one frame.
|
||||||
|
const MaxDataLength = 1<<24 - 1
|
||||||
|
|
||||||
|
// headerValueSepator separates multiple header values.
|
||||||
|
const headerValueSeparator = "\x00"
|
||||||
|
|
||||||
|
// Frame is a single SPDY frame in its unpacked in-memory representation. Use
|
||||||
|
// Framer to read and write it.
|
||||||
|
type Frame interface {
|
||||||
|
write(f *Framer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ControlFrameHeader contains all the fields in a control frame header,
|
||||||
|
// in its unpacked in-memory representation.
|
||||||
|
type ControlFrameHeader struct {
|
||||||
|
// Note, high bit is the "Control" bit.
|
||||||
|
version uint16 // spdy version number
|
||||||
|
frameType ControlFrameType
|
||||||
|
Flags ControlFlags
|
||||||
|
length uint32 // length of data field
|
||||||
|
}
|
||||||
|
|
||||||
|
type controlFrame interface {
|
||||||
|
Frame
|
||||||
|
read(h ControlFrameHeader, f *Framer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamId represents a 31-bit value identifying the stream.
|
||||||
|
type StreamId uint32
|
||||||
|
|
||||||
|
// SynStreamFrame is the unpacked, in-memory representation of a SYN_STREAM
|
||||||
|
// frame.
|
||||||
|
type SynStreamFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
StreamId StreamId
|
||||||
|
AssociatedToStreamId StreamId // stream id for a stream which this stream is associated to
|
||||||
|
Priority uint8 // priority of this frame (3-bit)
|
||||||
|
Slot uint8 // index in the server's credential vector of the client certificate
|
||||||
|
Headers http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// SynReplyFrame is the unpacked, in-memory representation of a SYN_REPLY frame.
|
||||||
|
type SynReplyFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
StreamId StreamId
|
||||||
|
Headers http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// RstStreamStatus represents the status that led to a RST_STREAM.
|
||||||
|
type RstStreamStatus uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProtocolError RstStreamStatus = iota + 1
|
||||||
|
InvalidStream
|
||||||
|
RefusedStream
|
||||||
|
UnsupportedVersion
|
||||||
|
Cancel
|
||||||
|
InternalError
|
||||||
|
FlowControlError
|
||||||
|
StreamInUse
|
||||||
|
StreamAlreadyClosed
|
||||||
|
InvalidCredentials
|
||||||
|
FrameTooLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
// RstStreamFrame is the unpacked, in-memory representation of a RST_STREAM
|
||||||
|
// frame.
|
||||||
|
type RstStreamFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
StreamId StreamId
|
||||||
|
Status RstStreamStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsFlag represents a flag in a SETTINGS frame.
|
||||||
|
type SettingsFlag uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagSettingsPersistValue SettingsFlag = 0x1
|
||||||
|
FlagSettingsPersisted = 0x2
|
||||||
|
)
|
||||||
|
|
||||||
|
// SettingsFlag represents the id of an id/value pair in a SETTINGS frame.
|
||||||
|
type SettingsId uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SettingsUploadBandwidth SettingsId = iota + 1
|
||||||
|
SettingsDownloadBandwidth
|
||||||
|
SettingsRoundTripTime
|
||||||
|
SettingsMaxConcurrentStreams
|
||||||
|
SettingsCurrentCwnd
|
||||||
|
SettingsDownloadRetransRate
|
||||||
|
SettingsInitialWindowSize
|
||||||
|
SettingsClientCretificateVectorSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// SettingsFlagIdValue is the unpacked, in-memory representation of the
|
||||||
|
// combined flag/id/value for a setting in a SETTINGS frame.
|
||||||
|
type SettingsFlagIdValue struct {
|
||||||
|
Flag SettingsFlag
|
||||||
|
Id SettingsId
|
||||||
|
Value uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsFrame is the unpacked, in-memory representation of a SPDY
|
||||||
|
// SETTINGS frame.
|
||||||
|
type SettingsFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
FlagIdValues []SettingsFlagIdValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// PingFrame is the unpacked, in-memory representation of a PING frame.
|
||||||
|
type PingFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
Id uint32 // unique id for this ping, from server is even, from client is odd.
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoAwayStatus represents the status in a GoAwayFrame.
|
||||||
|
type GoAwayStatus uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
GoAwayOK GoAwayStatus = iota
|
||||||
|
GoAwayProtocolError
|
||||||
|
GoAwayInternalError
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoAwayFrame is the unpacked, in-memory representation of a GOAWAY frame.
|
||||||
|
type GoAwayFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
LastGoodStreamId StreamId // last stream id which was accepted by sender
|
||||||
|
Status GoAwayStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadersFrame is the unpacked, in-memory representation of a HEADERS frame.
|
||||||
|
type HeadersFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
StreamId StreamId
|
||||||
|
Headers http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowUpdateFrame is the unpacked, in-memory representation of a
|
||||||
|
// WINDOW_UPDATE frame.
|
||||||
|
type WindowUpdateFrame struct {
|
||||||
|
CFHeader ControlFrameHeader
|
||||||
|
StreamId StreamId
|
||||||
|
DeltaWindowSize uint32 // additional number of bytes to existing window size
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement credential frame and related methods.
|
||||||
|
|
||||||
|
// DataFrame is the unpacked, in-memory representation of a DATA frame.
|
||||||
|
type DataFrame struct {
|
||||||
|
// Note, high bit is the "Control" bit. Should be 0 for data frames.
|
||||||
|
StreamId StreamId
|
||||||
|
Flags DataFlags
|
||||||
|
Data []byte // payload data of this frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SPDY specific error.
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnlowercasedHeaderName ErrorCode = "header was not lowercased"
|
||||||
|
DuplicateHeaders = "multiple headers with same name"
|
||||||
|
WrongCompressedPayloadSize = "compressed payload size was incorrect"
|
||||||
|
UnknownFrameType = "unknown frame type"
|
||||||
|
InvalidControlFrame = "invalid control frame"
|
||||||
|
InvalidDataFrame = "invalid data frame"
|
||||||
|
InvalidHeaderPresent = "frame contained invalid header"
|
||||||
|
ZeroStreamId = "stream id zero is disallowed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error contains both the type of error and additional values. StreamId is 0
|
||||||
|
// if Error is not associated with a stream.
|
||||||
|
type Error struct {
|
||||||
|
Err ErrorCode
|
||||||
|
StreamId StreamId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return string(e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidReqHeaders = map[string]bool{
|
||||||
|
"Connection": true,
|
||||||
|
"Host": true,
|
||||||
|
"Keep-Alive": true,
|
||||||
|
"Proxy-Connection": true,
|
||||||
|
"Transfer-Encoding": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidRespHeaders = map[string]bool{
|
||||||
|
"Connection": true,
|
||||||
|
"Keep-Alive": true,
|
||||||
|
"Proxy-Connection": true,
|
||||||
|
"Transfer-Encoding": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Framer handles serializing/deserializing SPDY frames, including compressing/
|
||||||
|
// decompressing payloads.
|
||||||
|
type Framer struct {
|
||||||
|
headerCompressionDisabled bool
|
||||||
|
w io.Writer
|
||||||
|
headerBuf *bytes.Buffer
|
||||||
|
headerCompressor *zlib.Writer
|
||||||
|
r io.Reader
|
||||||
|
headerReader io.LimitedReader
|
||||||
|
headerDecompressor io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFramer allocates a new Framer for a given SPDY connection, represented by
|
||||||
|
// a io.Writer and io.Reader. Note that Framer will read and write individual fields
|
||||||
|
// from/to the Reader and Writer, so the caller should pass in an appropriately
|
||||||
|
// buffered implementation to optimize performance.
|
||||||
|
func NewFramer(w io.Writer, r io.Reader) (*Framer, error) {
|
||||||
|
compressBuf := new(bytes.Buffer)
|
||||||
|
compressor, err := zlib.NewWriterLevelDict(compressBuf, zlib.BestCompression, []byte(headerDictionary))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
framer := &Framer{
|
||||||
|
w: w,
|
||||||
|
headerBuf: compressBuf,
|
||||||
|
headerCompressor: compressor,
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
return framer, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package spdy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (frame *SynStreamFrame) write(f *Framer) error {
|
||||||
|
return f.writeSynStreamFrame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *SynReplyFrame) write(f *Framer) error {
|
||||||
|
return f.writeSynReplyFrame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *RstStreamFrame) write(f *Framer) (err error) {
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeRstStream
|
||||||
|
frame.CFHeader.Flags = 0
|
||||||
|
frame.CFHeader.length = 8
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.Status == 0 {
|
||||||
|
return &Error{InvalidControlFrame, frame.StreamId}
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.Status); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *SettingsFrame) write(f *Framer) (err error) {
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeSettings
|
||||||
|
frame.CFHeader.length = uint32(len(frame.FlagIdValues)*8 + 4)
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, uint32(len(frame.FlagIdValues))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, flagIdValue := range frame.FlagIdValues {
|
||||||
|
flagId := uint32(flagIdValue.Flag)<<24 | uint32(flagIdValue.Id)
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, flagId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, flagIdValue.Value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *PingFrame) write(f *Framer) (err error) {
|
||||||
|
if frame.Id == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypePing
|
||||||
|
frame.CFHeader.Flags = 0
|
||||||
|
frame.CFHeader.length = 4
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.Id); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *GoAwayFrame) write(f *Framer) (err error) {
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeGoAway
|
||||||
|
frame.CFHeader.Flags = 0
|
||||||
|
frame.CFHeader.length = 8
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.LastGoodStreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.Status); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *HeadersFrame) write(f *Framer) error {
|
||||||
|
return f.writeHeadersFrame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *WindowUpdateFrame) write(f *Framer) (err error) {
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeWindowUpdate
|
||||||
|
frame.CFHeader.Flags = 0
|
||||||
|
frame.CFHeader.length = 8
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.DeltaWindowSize); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frame *DataFrame) write(f *Framer) error {
|
||||||
|
return f.writeDataFrame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFrame writes a frame.
|
||||||
|
func (f *Framer) WriteFrame(frame Frame) error {
|
||||||
|
return frame.write(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeControlFrameHeader(w io.Writer, h ControlFrameHeader) error {
|
||||||
|
if err := binary.Write(w, binary.BigEndian, 0x8000|h.version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := binary.Write(w, binary.BigEndian, h.frameType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flagsAndLength := uint32(h.Flags)<<24 | h.length
|
||||||
|
if err := binary.Write(w, binary.BigEndian, flagsAndLength); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeHeaderValueBlock(w io.Writer, h http.Header) (n int, err error) {
|
||||||
|
n = 0
|
||||||
|
if err = binary.Write(w, binary.BigEndian, uint32(len(h))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += 2
|
||||||
|
for name, values := range h {
|
||||||
|
if err = binary.Write(w, binary.BigEndian, uint32(len(name))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += 2
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
if _, err = io.WriteString(w, name); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += len(name)
|
||||||
|
v := strings.Join(values, headerValueSeparator)
|
||||||
|
if err = binary.Write(w, binary.BigEndian, uint32(len(v))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += 2
|
||||||
|
if _, err = io.WriteString(w, v); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += len(v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) writeSynStreamFrame(frame *SynStreamFrame) (err error) {
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
// Marshal the headers.
|
||||||
|
var writer io.Writer = f.headerBuf
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
writer = f.headerCompressor
|
||||||
|
}
|
||||||
|
if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
f.headerCompressor.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ControlFrameHeader.
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeSynStream
|
||||||
|
frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 10)
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.AssociatedToStreamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.Priority<<5); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.Slot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.headerBuf.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) writeSynReplyFrame(frame *SynReplyFrame) (err error) {
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
// Marshal the headers.
|
||||||
|
var writer io.Writer = f.headerBuf
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
writer = f.headerCompressor
|
||||||
|
}
|
||||||
|
if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
f.headerCompressor.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ControlFrameHeader.
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeSynReply
|
||||||
|
frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 4)
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.headerBuf.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) writeHeadersFrame(frame *HeadersFrame) (err error) {
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
// Marshal the headers.
|
||||||
|
var writer io.Writer = f.headerBuf
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
writer = f.headerCompressor
|
||||||
|
}
|
||||||
|
if _, err = writeHeaderValueBlock(writer, frame.Headers); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !f.headerCompressionDisabled {
|
||||||
|
f.headerCompressor.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ControlFrameHeader.
|
||||||
|
frame.CFHeader.version = Version
|
||||||
|
frame.CFHeader.frameType = TypeHeaders
|
||||||
|
frame.CFHeader.length = uint32(len(f.headerBuf.Bytes()) + 4)
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = writeControlFrameHeader(f.w, frame.CFHeader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = f.w.Write(f.headerBuf.Bytes()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.headerBuf.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framer) writeDataFrame(frame *DataFrame) (err error) {
|
||||||
|
if frame.StreamId == 0 {
|
||||||
|
return &Error{ZeroStreamId, 0}
|
||||||
|
}
|
||||||
|
if frame.StreamId&0x80000000 != 0 || len(frame.Data) > MaxDataLength {
|
||||||
|
return &Error{InvalidDataFrame, frame.StreamId}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize frame to Writer.
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, frame.StreamId); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flagsAndLength := uint32(frame.Flags)<<24 | uint32(len(frame.Data))
|
||||||
|
if err = binary.Write(f.w, binary.BigEndian, flagsAndLength); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = f.w.Write(frame.Data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
package spdystream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/spdystream/spdy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnreadPartialData = errors.New("unread partial data")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stream struct {
|
||||||
|
streamId spdy.StreamId
|
||||||
|
parent *Stream
|
||||||
|
conn *Connection
|
||||||
|
startChan chan error
|
||||||
|
|
||||||
|
dataLock sync.RWMutex
|
||||||
|
dataChan chan []byte
|
||||||
|
unread []byte
|
||||||
|
|
||||||
|
priority uint8
|
||||||
|
headers http.Header
|
||||||
|
headerChan chan http.Header
|
||||||
|
finishLock sync.Mutex
|
||||||
|
finished bool
|
||||||
|
replyCond *sync.Cond
|
||||||
|
replied bool
|
||||||
|
closeLock sync.Mutex
|
||||||
|
closeChan chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteData writes data to stream, sending a dataframe per call
|
||||||
|
func (s *Stream) WriteData(data []byte, fin bool) error {
|
||||||
|
s.waitWriteReply()
|
||||||
|
var flags spdy.DataFlags
|
||||||
|
|
||||||
|
if fin {
|
||||||
|
flags = spdy.DataFlagFin
|
||||||
|
s.finishLock.Lock()
|
||||||
|
if s.finished {
|
||||||
|
s.finishLock.Unlock()
|
||||||
|
return ErrWriteClosedStream
|
||||||
|
}
|
||||||
|
s.finished = true
|
||||||
|
s.finishLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataFrame := &spdy.DataFrame{
|
||||||
|
StreamId: s.streamId,
|
||||||
|
Flags: flags,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
debugMessage("(%p) (%d) Writing data frame", s, s.streamId)
|
||||||
|
return s.conn.framer.WriteFrame(dataFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes bytes to a stream, calling write data for each call.
|
||||||
|
func (s *Stream) Write(data []byte) (n int, err error) {
|
||||||
|
err = s.WriteData(data, false)
|
||||||
|
if err == nil {
|
||||||
|
n = len(data)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads bytes from a stream, a single read will never get more
|
||||||
|
// than what is sent on a single data frame, but a multiple calls to
|
||||||
|
// read may get data from the same data frame.
|
||||||
|
func (s *Stream) Read(p []byte) (n int, err error) {
|
||||||
|
if s.unread == nil {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
return 0, io.EOF
|
||||||
|
case read, ok := <-s.dataChan:
|
||||||
|
if !ok {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
s.unread = read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = copy(p, s.unread)
|
||||||
|
if n < len(s.unread) {
|
||||||
|
s.unread = s.unread[n:]
|
||||||
|
} else {
|
||||||
|
s.unread = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadData reads an entire data frame and returns the byte array
|
||||||
|
// from the data frame. If there is unread data from the result
|
||||||
|
// of a Read call, this function will return an ErrUnreadPartialData.
|
||||||
|
func (s *Stream) ReadData() ([]byte, error) {
|
||||||
|
debugMessage("(%p) Reading data from %d", s, s.streamId)
|
||||||
|
if s.unread != nil {
|
||||||
|
return nil, ErrUnreadPartialData
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
return nil, io.EOF
|
||||||
|
case read, ok := <-s.dataChan:
|
||||||
|
if !ok {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
return read, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) waitWriteReply() {
|
||||||
|
if s.replyCond != nil {
|
||||||
|
s.replyCond.L.Lock()
|
||||||
|
for !s.replied {
|
||||||
|
s.replyCond.Wait()
|
||||||
|
}
|
||||||
|
s.replyCond.L.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the stream to receive a reply.
|
||||||
|
func (s *Stream) Wait() error {
|
||||||
|
return s.WaitTimeout(time.Duration(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitTimeout waits for the stream to receive a reply or for timeout.
|
||||||
|
// When the timeout is reached, ErrTimeout will be returned.
|
||||||
|
func (s *Stream) WaitTimeout(timeout time.Duration) error {
|
||||||
|
var timeoutChan <-chan time.Time
|
||||||
|
if timeout > time.Duration(0) {
|
||||||
|
timeoutChan = time.After(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-s.startChan:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case <-timeoutChan:
|
||||||
|
return ErrTimeout
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the stream by sending an empty data frame with the
|
||||||
|
// finish flag set, indicating this side is finished with the stream.
|
||||||
|
func (s *Stream) Close() error {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
// Stream is now fully closed
|
||||||
|
s.conn.removeStream(s)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return s.WriteData([]byte{}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset sends a reset frame, putting the stream into the fully closed state.
|
||||||
|
func (s *Stream) Reset() error {
|
||||||
|
s.conn.removeStream(s)
|
||||||
|
return s.resetStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) resetStream() error {
|
||||||
|
// Always call closeRemoteChannels, even if s.finished is already true.
|
||||||
|
// This makes it so that stream.Close() followed by stream.Reset() allows
|
||||||
|
// stream.Read() to unblock.
|
||||||
|
s.closeRemoteChannels()
|
||||||
|
|
||||||
|
s.finishLock.Lock()
|
||||||
|
if s.finished {
|
||||||
|
s.finishLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.finished = true
|
||||||
|
s.finishLock.Unlock()
|
||||||
|
|
||||||
|
resetFrame := &spdy.RstStreamFrame{
|
||||||
|
StreamId: s.streamId,
|
||||||
|
Status: spdy.Cancel,
|
||||||
|
}
|
||||||
|
return s.conn.framer.WriteFrame(resetFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSubStream creates a stream using the current as the parent
|
||||||
|
func (s *Stream) CreateSubStream(headers http.Header, fin bool) (*Stream, error) {
|
||||||
|
return s.conn.CreateStream(headers, s, fin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPriority sets the stream priority, does not affect the
|
||||||
|
// remote priority of this stream after Open has been called.
|
||||||
|
// Valid values are 0 through 7, 0 being the highest priority
|
||||||
|
// and 7 the lowest.
|
||||||
|
func (s *Stream) SetPriority(priority uint8) {
|
||||||
|
s.priority = priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHeader sends a header frame across the stream
|
||||||
|
func (s *Stream) SendHeader(headers http.Header, fin bool) error {
|
||||||
|
return s.conn.sendHeaders(headers, s, fin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendReply sends a reply on a stream, only valid to be called once
|
||||||
|
// when handling a new stream
|
||||||
|
func (s *Stream) SendReply(headers http.Header, fin bool) error {
|
||||||
|
if s.replyCond == nil {
|
||||||
|
return errors.New("cannot reply on initiated stream")
|
||||||
|
}
|
||||||
|
s.replyCond.L.Lock()
|
||||||
|
defer s.replyCond.L.Unlock()
|
||||||
|
if s.replied {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.conn.sendReply(headers, s, fin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.replied = true
|
||||||
|
s.replyCond.Broadcast()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refuse sends a reset frame with the status refuse, only
|
||||||
|
// valid to be called once when handling a new stream. This
|
||||||
|
// may be used to indicate that a stream is not allowed
|
||||||
|
// when http status codes are not being used.
|
||||||
|
func (s *Stream) Refuse() error {
|
||||||
|
if s.replied {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.replied = true
|
||||||
|
return s.conn.sendReset(spdy.RefusedStream, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel sends a reset frame with the status canceled. This
|
||||||
|
// can be used at any time by the creator of the Stream to
|
||||||
|
// indicate the stream is no longer needed.
|
||||||
|
func (s *Stream) Cancel() error {
|
||||||
|
return s.conn.sendReset(spdy.Cancel, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReceiveHeader receives a header sent on the other side
|
||||||
|
// of the stream. This function will block until a header
|
||||||
|
// is received or stream is closed.
|
||||||
|
func (s *Stream) ReceiveHeader() (http.Header, error) {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
break
|
||||||
|
case header, ok := <-s.headerChan:
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("header chan closed")
|
||||||
|
}
|
||||||
|
return header, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("stream closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns the parent stream
|
||||||
|
func (s *Stream) Parent() *Stream {
|
||||||
|
return s.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the headers used to create the stream
|
||||||
|
func (s *Stream) Headers() http.Header {
|
||||||
|
return s.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string version of stream using the
|
||||||
|
// streamId to uniquely identify the stream
|
||||||
|
func (s *Stream) String() string {
|
||||||
|
return fmt.Sprintf("stream:%d", s.streamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier returns a 32 bit identifier for the stream
|
||||||
|
func (s *Stream) Identifier() uint32 {
|
||||||
|
return uint32(s.streamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFinished returns whether the stream has finished
|
||||||
|
// sending data
|
||||||
|
func (s *Stream) IsFinished() bool {
|
||||||
|
return s.finished
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement net.Conn interface
|
||||||
|
|
||||||
|
func (s *Stream) LocalAddr() net.Addr {
|
||||||
|
return s.conn.conn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) RemoteAddr() net.Addr {
|
||||||
|
return s.conn.conn.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO set per stream values instead of connection-wide
|
||||||
|
|
||||||
|
func (s *Stream) SetDeadline(t time.Time) error {
|
||||||
|
return s.conn.conn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) SetReadDeadline(t time.Time) error {
|
||||||
|
return s.conn.conn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) SetWriteDeadline(t time.Time) error {
|
||||||
|
return s.conn.conn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) closeRemoteChannels() {
|
||||||
|
s.closeLock.Lock()
|
||||||
|
defer s.closeLock.Unlock()
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
default:
|
||||||
|
close(s.closeChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package spdystream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DEBUG = os.Getenv("DEBUG")
|
||||||
|
)
|
||||||
|
|
||||||
|
func debugMessage(fmt string, args ...interface{}) {
|
||||||
|
if DEBUG != "" {
|
||||||
|
log.Printf(fmt, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2012,2013 Ernest Micklei
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OBSOLETE : use restful.DefaultContainer.EnableContentEncoding(true) to change this setting.
|
||||||
|
var EnableContentEncoding = false
|
||||||
|
|
||||||
|
// CompressingResponseWriter is a http.ResponseWriter that can perform content encoding (gzip and zlib)
|
||||||
|
type CompressingResponseWriter struct {
|
||||||
|
writer http.ResponseWriter
|
||||||
|
compressor io.WriteCloser
|
||||||
|
encoding string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header is part of http.ResponseWriter interface
|
||||||
|
func (c *CompressingResponseWriter) Header() http.Header {
|
||||||
|
return c.writer.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader is part of http.ResponseWriter interface
|
||||||
|
func (c *CompressingResponseWriter) WriteHeader(status int) {
|
||||||
|
c.writer.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write is part of http.ResponseWriter interface
|
||||||
|
// It is passed through the compressor
|
||||||
|
func (c *CompressingResponseWriter) Write(bytes []byte) (int, error) {
|
||||||
|
if c.isCompressorClosed() {
|
||||||
|
return -1, errors.New("Compressing error: tried to write data using closed compressor")
|
||||||
|
}
|
||||||
|
return c.compressor.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseNotify is part of http.CloseNotifier interface
|
||||||
|
func (c *CompressingResponseWriter) CloseNotify() <-chan bool {
|
||||||
|
return c.writer.(http.CloseNotifier).CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the underlying compressor
|
||||||
|
func (c *CompressingResponseWriter) Close() error {
|
||||||
|
if c.isCompressorClosed() {
|
||||||
|
return errors.New("Compressing error: tried to close already closed compressor")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.compressor.Close()
|
||||||
|
if ENCODING_GZIP == c.encoding {
|
||||||
|
currentCompressorProvider.ReleaseGzipWriter(c.compressor.(*gzip.Writer))
|
||||||
|
}
|
||||||
|
if ENCODING_DEFLATE == c.encoding {
|
||||||
|
currentCompressorProvider.ReleaseZlibWriter(c.compressor.(*zlib.Writer))
|
||||||
|
}
|
||||||
|
// gc hint needed?
|
||||||
|
c.compressor = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CompressingResponseWriter) isCompressorClosed() bool {
|
||||||
|
return nil == c.compressor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack implements the Hijacker interface
|
||||||
|
// This is especially useful when combining Container.EnabledContentEncoding
|
||||||
|
// in combination with websockets (for instance gorilla/websocket)
|
||||||
|
func (c *CompressingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker, ok := c.writer.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("ResponseWriter doesn't support Hijacker interface")
|
||||||
|
}
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantsCompressedResponse reads the Accept-Encoding header to see if and which encoding is requested.
|
||||||
|
func wantsCompressedResponse(httpRequest *http.Request) (bool, string) {
|
||||||
|
header := httpRequest.Header.Get(HEADER_AcceptEncoding)
|
||||||
|
gi := strings.Index(header, ENCODING_GZIP)
|
||||||
|
zi := strings.Index(header, ENCODING_DEFLATE)
|
||||||
|
// use in order of appearance
|
||||||
|
if gi == -1 {
|
||||||
|
return zi != -1, ENCODING_DEFLATE
|
||||||
|
} else if zi == -1 {
|
||||||
|
return gi != -1, ENCODING_GZIP
|
||||||
|
} else {
|
||||||
|
if gi < zi {
|
||||||
|
return true, ENCODING_GZIP
|
||||||
|
}
|
||||||
|
return true, ENCODING_DEFLATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCompressingResponseWriter create a CompressingResponseWriter for a known encoding = {gzip,deflate}
|
||||||
|
func NewCompressingResponseWriter(httpWriter http.ResponseWriter, encoding string) (*CompressingResponseWriter, error) {
|
||||||
|
httpWriter.Header().Set(HEADER_ContentEncoding, encoding)
|
||||||
|
c := new(CompressingResponseWriter)
|
||||||
|
c.writer = httpWriter
|
||||||
|
var err error
|
||||||
|
if ENCODING_GZIP == encoding {
|
||||||
|
w := currentCompressorProvider.AcquireGzipWriter()
|
||||||
|
w.Reset(httpWriter)
|
||||||
|
c.compressor = w
|
||||||
|
c.encoding = ENCODING_GZIP
|
||||||
|
} else if ENCODING_DEFLATE == encoding {
|
||||||
|
w := currentCompressorProvider.AcquireZlibWriter()
|
||||||
|
w.Reset(httpWriter)
|
||||||
|
c.compressor = w
|
||||||
|
c.encoding = ENCODING_DEFLATE
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("Unknown encoding:" + encoding)
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoundedCachedCompressors is a CompressorProvider that uses a cache with a fixed amount
|
||||||
|
// of writers and readers (resources).
|
||||||
|
// If a new resource is acquired and all are in use, it will return a new unmanaged resource.
|
||||||
|
type BoundedCachedCompressors struct {
|
||||||
|
gzipWriters chan *gzip.Writer
|
||||||
|
gzipReaders chan *gzip.Reader
|
||||||
|
zlibWriters chan *zlib.Writer
|
||||||
|
writersCapacity int
|
||||||
|
readersCapacity int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBoundedCachedCompressors returns a new, with filled cache, BoundedCachedCompressors.
|
||||||
|
func NewBoundedCachedCompressors(writersCapacity, readersCapacity int) *BoundedCachedCompressors {
|
||||||
|
b := &BoundedCachedCompressors{
|
||||||
|
gzipWriters: make(chan *gzip.Writer, writersCapacity),
|
||||||
|
gzipReaders: make(chan *gzip.Reader, readersCapacity),
|
||||||
|
zlibWriters: make(chan *zlib.Writer, writersCapacity),
|
||||||
|
writersCapacity: writersCapacity,
|
||||||
|
readersCapacity: readersCapacity,
|
||||||
|
}
|
||||||
|
for ix := 0; ix < writersCapacity; ix++ {
|
||||||
|
b.gzipWriters <- newGzipWriter()
|
||||||
|
b.zlibWriters <- newZlibWriter()
|
||||||
|
}
|
||||||
|
for ix := 0; ix < readersCapacity; ix++ {
|
||||||
|
b.gzipReaders <- newGzipReader()
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireGzipWriter returns an resettable *gzip.Writer. Needs to be released.
|
||||||
|
func (b *BoundedCachedCompressors) AcquireGzipWriter() *gzip.Writer {
|
||||||
|
var writer *gzip.Writer
|
||||||
|
select {
|
||||||
|
case writer, _ = <-b.gzipWriters:
|
||||||
|
default:
|
||||||
|
// return a new unmanaged one
|
||||||
|
writer = newGzipWriter()
|
||||||
|
}
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseGzipWriter accepts a writer (does not have to be one that was cached)
|
||||||
|
// only when the cache has room for it. It will ignore it otherwise.
|
||||||
|
func (b *BoundedCachedCompressors) ReleaseGzipWriter(w *gzip.Writer) {
|
||||||
|
// forget the unmanaged ones
|
||||||
|
if len(b.gzipWriters) < b.writersCapacity {
|
||||||
|
b.gzipWriters <- w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireGzipReader returns a *gzip.Reader. Needs to be released.
|
||||||
|
func (b *BoundedCachedCompressors) AcquireGzipReader() *gzip.Reader {
|
||||||
|
var reader *gzip.Reader
|
||||||
|
select {
|
||||||
|
case reader, _ = <-b.gzipReaders:
|
||||||
|
default:
|
||||||
|
// return a new unmanaged one
|
||||||
|
reader = newGzipReader()
|
||||||
|
}
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseGzipReader accepts a reader (does not have to be one that was cached)
|
||||||
|
// only when the cache has room for it. It will ignore it otherwise.
|
||||||
|
func (b *BoundedCachedCompressors) ReleaseGzipReader(r *gzip.Reader) {
|
||||||
|
// forget the unmanaged ones
|
||||||
|
if len(b.gzipReaders) < b.readersCapacity {
|
||||||
|
b.gzipReaders <- r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireZlibWriter returns an resettable *zlib.Writer. Needs to be released.
|
||||||
|
func (b *BoundedCachedCompressors) AcquireZlibWriter() *zlib.Writer {
|
||||||
|
var writer *zlib.Writer
|
||||||
|
select {
|
||||||
|
case writer, _ = <-b.zlibWriters:
|
||||||
|
default:
|
||||||
|
// return a new unmanaged one
|
||||||
|
writer = newZlibWriter()
|
||||||
|
}
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseZlibWriter accepts a writer (does not have to be one that was cached)
|
||||||
|
// only when the cache has room for it. It will ignore it otherwise.
|
||||||
|
func (b *BoundedCachedCompressors) ReleaseZlibWriter(w *zlib.Writer) {
|
||||||
|
// forget the unmanaged ones
|
||||||
|
if len(b.zlibWriters) < b.writersCapacity {
|
||||||
|
b.zlibWriters <- w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncPoolCompessors is a CompressorProvider that use the standard sync.Pool.
|
||||||
|
type SyncPoolCompessors struct {
|
||||||
|
GzipWriterPool *sync.Pool
|
||||||
|
GzipReaderPool *sync.Pool
|
||||||
|
ZlibWriterPool *sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSyncPoolCompessors returns a new ("empty") SyncPoolCompessors.
|
||||||
|
func NewSyncPoolCompessors() *SyncPoolCompessors {
|
||||||
|
return &SyncPoolCompessors{
|
||||||
|
GzipWriterPool: &sync.Pool{
|
||||||
|
New: func() interface{} { return newGzipWriter() },
|
||||||
|
},
|
||||||
|
GzipReaderPool: &sync.Pool{
|
||||||
|
New: func() interface{} { return newGzipReader() },
|
||||||
|
},
|
||||||
|
ZlibWriterPool: &sync.Pool{
|
||||||
|
New: func() interface{} { return newZlibWriter() },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) AcquireGzipWriter() *gzip.Writer {
|
||||||
|
return s.GzipWriterPool.Get().(*gzip.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) ReleaseGzipWriter(w *gzip.Writer) {
|
||||||
|
s.GzipWriterPool.Put(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) AcquireGzipReader() *gzip.Reader {
|
||||||
|
return s.GzipReaderPool.Get().(*gzip.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) ReleaseGzipReader(r *gzip.Reader) {
|
||||||
|
s.GzipReaderPool.Put(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) AcquireZlibWriter() *zlib.Writer {
|
||||||
|
return s.ZlibWriterPool.Get().(*zlib.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SyncPoolCompessors) ReleaseZlibWriter(w *zlib.Writer) {
|
||||||
|
s.ZlibWriterPool.Put(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGzipWriter() *gzip.Writer {
|
||||||
|
// create with an empty bytes writer; it will be replaced before using the gzipWriter
|
||||||
|
writer, err := gzip.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGzipReader() *gzip.Reader {
|
||||||
|
// create with an empty reader (but with GZIP header); it will be replaced before using the gzipReader
|
||||||
|
// we can safely use currentCompressProvider because it is set on package initialization.
|
||||||
|
w := currentCompressorProvider.AcquireGzipWriter()
|
||||||
|
defer currentCompressorProvider.ReleaseGzipWriter(w)
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
w.Reset(b)
|
||||||
|
w.Flush()
|
||||||
|
w.Close()
|
||||||
|
reader, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func newZlibWriter() *zlib.Writer {
|
||||||
|
writer, err := zlib.NewWriterLevel(new(bytes.Buffer), gzip.BestSpeed)
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"compress/zlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompressorProvider describes a component that can provider compressors for the std methods.
|
||||||
|
type CompressorProvider interface {
|
||||||
|
// Returns a *gzip.Writer which needs to be released later.
|
||||||
|
// Before using it, call Reset().
|
||||||
|
AcquireGzipWriter() *gzip.Writer
|
||||||
|
|
||||||
|
// Releases an acquired *gzip.Writer.
|
||||||
|
ReleaseGzipWriter(w *gzip.Writer)
|
||||||
|
|
||||||
|
// Returns a *gzip.Reader which needs to be released later.
|
||||||
|
AcquireGzipReader() *gzip.Reader
|
||||||
|
|
||||||
|
// Releases an acquired *gzip.Reader.
|
||||||
|
ReleaseGzipReader(w *gzip.Reader)
|
||||||
|
|
||||||
|
// Returns a *zlib.Writer which needs to be released later.
|
||||||
|
// Before using it, call Reset().
|
||||||
|
AcquireZlibWriter() *zlib.Writer
|
||||||
|
|
||||||
|
// Releases an acquired *zlib.Writer.
|
||||||
|
ReleaseZlibWriter(w *zlib.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCompressorProvider is the actual provider of compressors (zlib or gzip).
|
||||||
|
var currentCompressorProvider CompressorProvider
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
currentCompressorProvider = NewSyncPoolCompessors()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentCompressorProvider returns the current CompressorProvider.
|
||||||
|
// It is initialized using a SyncPoolCompessors.
|
||||||
|
func CurrentCompressorProvider() CompressorProvider {
|
||||||
|
return currentCompressorProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCompressorProvider sets the actual provider of compressors (zlib or gzip).
|
||||||
|
func SetCompressorProvider(p CompressorProvider) {
|
||||||
|
if p == nil {
|
||||||
|
panic("cannot set compressor provider to nil")
|
||||||
|
}
|
||||||
|
currentCompressorProvider = p
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
const (
|
||||||
|
MIME_XML = "application/xml" // Accept or Content-Type used in Consumes() and/or Produces()
|
||||||
|
MIME_JSON = "application/json" // Accept or Content-Type used in Consumes() and/or Produces()
|
||||||
|
MIME_OCTET = "application/octet-stream" // If Content-Type is not present in request, use the default
|
||||||
|
|
||||||
|
HEADER_Allow = "Allow"
|
||||||
|
HEADER_Accept = "Accept"
|
||||||
|
HEADER_Origin = "Origin"
|
||||||
|
HEADER_ContentType = "Content-Type"
|
||||||
|
HEADER_LastModified = "Last-Modified"
|
||||||
|
HEADER_AcceptEncoding = "Accept-Encoding"
|
||||||
|
HEADER_ContentEncoding = "Content-Encoding"
|
||||||
|
HEADER_AccessControlExposeHeaders = "Access-Control-Expose-Headers"
|
||||||
|
HEADER_AccessControlRequestMethod = "Access-Control-Request-Method"
|
||||||
|
HEADER_AccessControlRequestHeaders = "Access-Control-Request-Headers"
|
||||||
|
HEADER_AccessControlAllowMethods = "Access-Control-Allow-Methods"
|
||||||
|
HEADER_AccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||||
|
HEADER_AccessControlAllowCredentials = "Access-Control-Allow-Credentials"
|
||||||
|
HEADER_AccessControlAllowHeaders = "Access-Control-Allow-Headers"
|
||||||
|
HEADER_AccessControlMaxAge = "Access-Control-Max-Age"
|
||||||
|
|
||||||
|
ENCODING_GZIP = "gzip"
|
||||||
|
ENCODING_DEFLATE = "deflate"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Container holds a collection of WebServices and a http.ServeMux to dispatch http requests.
|
||||||
|
// The requests are further dispatched to routes of WebServices using a RouteSelector
|
||||||
|
type Container struct {
|
||||||
|
webServicesLock sync.RWMutex
|
||||||
|
webServices []*WebService
|
||||||
|
ServeMux *http.ServeMux
|
||||||
|
isRegisteredOnRoot bool
|
||||||
|
containerFilters []FilterFunction
|
||||||
|
doNotRecover bool // default is true
|
||||||
|
recoverHandleFunc RecoverHandleFunction
|
||||||
|
serviceErrorHandleFunc ServiceErrorHandleFunction
|
||||||
|
router RouteSelector // default is a CurlyRouter (RouterJSR311 is a slower alternative)
|
||||||
|
contentEncodingEnabled bool // default is false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContainer creates a new Container using a new ServeMux and default router (CurlyRouter)
|
||||||
|
func NewContainer() *Container {
|
||||||
|
return &Container{
|
||||||
|
webServices: []*WebService{},
|
||||||
|
ServeMux: http.NewServeMux(),
|
||||||
|
isRegisteredOnRoot: false,
|
||||||
|
containerFilters: []FilterFunction{},
|
||||||
|
doNotRecover: true,
|
||||||
|
recoverHandleFunc: logStackOnRecover,
|
||||||
|
serviceErrorHandleFunc: writeServiceError,
|
||||||
|
router: CurlyRouter{},
|
||||||
|
contentEncodingEnabled: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverHandleFunction declares functions that can be used to handle a panic situation.
|
||||||
|
// The first argument is what recover() returns. The second must be used to communicate an error response.
|
||||||
|
type RecoverHandleFunction func(interface{}, http.ResponseWriter)
|
||||||
|
|
||||||
|
// RecoverHandler changes the default function (logStackOnRecover) to be called
|
||||||
|
// when a panic is detected. DoNotRecover must be have its default value (=false).
|
||||||
|
func (c *Container) RecoverHandler(handler RecoverHandleFunction) {
|
||||||
|
c.recoverHandleFunc = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceErrorHandleFunction declares functions that can be used to handle a service error situation.
|
||||||
|
// The first argument is the service error, the second is the request that resulted in the error and
|
||||||
|
// the third must be used to communicate an error response.
|
||||||
|
type ServiceErrorHandleFunction func(ServiceError, *Request, *Response)
|
||||||
|
|
||||||
|
// ServiceErrorHandler changes the default function (writeServiceError) to be called
|
||||||
|
// when a ServiceError is detected.
|
||||||
|
func (c *Container) ServiceErrorHandler(handler ServiceErrorHandleFunction) {
|
||||||
|
c.serviceErrorHandleFunc = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoNotRecover controls whether panics will be caught to return HTTP 500.
|
||||||
|
// If set to true, Route functions are responsible for handling any error situation.
|
||||||
|
// Default value is true.
|
||||||
|
func (c *Container) DoNotRecover(doNot bool) {
|
||||||
|
c.doNotRecover = doNot
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router changes the default Router (currently CurlyRouter)
|
||||||
|
func (c *Container) Router(aRouter RouteSelector) {
|
||||||
|
c.router = aRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableContentEncoding (default=false) allows for GZIP or DEFLATE encoding of responses.
|
||||||
|
func (c *Container) EnableContentEncoding(enabled bool) {
|
||||||
|
c.contentEncodingEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a WebService to the Container. It will detect duplicate root paths and exit in that case.
|
||||||
|
func (c *Container) Add(service *WebService) *Container {
|
||||||
|
c.webServicesLock.Lock()
|
||||||
|
defer c.webServicesLock.Unlock()
|
||||||
|
|
||||||
|
// if rootPath was not set then lazy initialize it
|
||||||
|
if len(service.rootPath) == 0 {
|
||||||
|
service.Path("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot have duplicate root paths
|
||||||
|
for _, each := range c.webServices {
|
||||||
|
if each.RootPath() == service.RootPath() {
|
||||||
|
log.Printf("[restful] WebService with duplicate root path detected:['%v']", each)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not registered on root then add specific mapping
|
||||||
|
if !c.isRegisteredOnRoot {
|
||||||
|
c.isRegisteredOnRoot = c.addHandler(service, c.ServeMux)
|
||||||
|
}
|
||||||
|
c.webServices = append(c.webServices, service)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// addHandler may set a new HandleFunc for the serveMux
|
||||||
|
// this function must run inside the critical region protected by the webServicesLock.
|
||||||
|
// returns true if the function was registered on root ("/")
|
||||||
|
func (c *Container) addHandler(service *WebService, serveMux *http.ServeMux) bool {
|
||||||
|
pattern := fixedPrefixPath(service.RootPath())
|
||||||
|
// check if root path registration is needed
|
||||||
|
if "/" == pattern || "" == pattern {
|
||||||
|
serveMux.HandleFunc("/", c.dispatch)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// detect if registration already exists
|
||||||
|
alreadyMapped := false
|
||||||
|
for _, each := range c.webServices {
|
||||||
|
if each.RootPath() == service.RootPath() {
|
||||||
|
alreadyMapped = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyMapped {
|
||||||
|
serveMux.HandleFunc(pattern, c.dispatch)
|
||||||
|
if !strings.HasSuffix(pattern, "/") {
|
||||||
|
serveMux.HandleFunc(pattern+"/", c.dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Remove(ws *WebService) error {
|
||||||
|
if c.ServeMux == http.DefaultServeMux {
|
||||||
|
errMsg := fmt.Sprintf("[restful] cannot remove a WebService from a Container using the DefaultServeMux: ['%v']", ws)
|
||||||
|
log.Print(errMsg)
|
||||||
|
return errors.New(errMsg)
|
||||||
|
}
|
||||||
|
c.webServicesLock.Lock()
|
||||||
|
defer c.webServicesLock.Unlock()
|
||||||
|
// build a new ServeMux and re-register all WebServices
|
||||||
|
newServeMux := http.NewServeMux()
|
||||||
|
newServices := []*WebService{}
|
||||||
|
newIsRegisteredOnRoot := false
|
||||||
|
for _, each := range c.webServices {
|
||||||
|
if each.rootPath != ws.rootPath {
|
||||||
|
// If not registered on root then add specific mapping
|
||||||
|
if !newIsRegisteredOnRoot {
|
||||||
|
newIsRegisteredOnRoot = c.addHandler(each, newServeMux)
|
||||||
|
}
|
||||||
|
newServices = append(newServices, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.webServices, c.ServeMux, c.isRegisteredOnRoot = newServices, newServeMux, newIsRegisteredOnRoot
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// logStackOnRecover is the default RecoverHandleFunction and is called
|
||||||
|
// when DoNotRecover is false and the recoverHandleFunc is not set for the container.
|
||||||
|
// Default implementation logs the stacktrace and writes the stacktrace on the response.
|
||||||
|
// This may be a security issue as it exposes sourcecode information.
|
||||||
|
func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteString(fmt.Sprintf("[restful] recover from panic situation: - %v\r\n", panicReason))
|
||||||
|
for i := 2; ; i += 1 {
|
||||||
|
_, file, line, ok := runtime.Caller(i)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buffer.WriteString(fmt.Sprintf(" %s:%d\r\n", file, line))
|
||||||
|
}
|
||||||
|
log.Print(buffer.String())
|
||||||
|
httpWriter.WriteHeader(http.StatusInternalServerError)
|
||||||
|
httpWriter.Write(buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeServiceError is the default ServiceErrorHandleFunction and is called
|
||||||
|
// when a ServiceError is returned during route selection. Default implementation
|
||||||
|
// calls resp.WriteErrorString(err.Code, err.Message)
|
||||||
|
func writeServiceError(err ServiceError, req *Request, resp *Response) {
|
||||||
|
resp.WriteErrorString(err.Code, err.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the incoming Http Request to a matching WebService.
|
||||||
|
func (c *Container) Dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
|
||||||
|
if httpWriter == nil {
|
||||||
|
panic("httpWriter cannot be nil")
|
||||||
|
}
|
||||||
|
if httpRequest == nil {
|
||||||
|
panic("httpRequest cannot be nil")
|
||||||
|
}
|
||||||
|
c.dispatch(httpWriter, httpRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch the incoming Http Request to a matching WebService.
|
||||||
|
func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
|
||||||
|
writer := httpWriter
|
||||||
|
|
||||||
|
// CompressingResponseWriter should be closed after all operations are done
|
||||||
|
defer func() {
|
||||||
|
if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
|
||||||
|
compressWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Instal panic recovery unless told otherwise
|
||||||
|
if !c.doNotRecover { // catch all for 500 response
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
c.recoverHandleFunc(r, writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if compression is needed
|
||||||
|
// assume without compression, test for override
|
||||||
|
if c.contentEncodingEnabled {
|
||||||
|
doCompress, encoding := wantsCompressedResponse(httpRequest)
|
||||||
|
if doCompress {
|
||||||
|
var err error
|
||||||
|
writer, err = NewCompressingResponseWriter(httpWriter, encoding)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("[restful] unable to install compressor: ", err)
|
||||||
|
httpWriter.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find best match Route ; err is non nil if no match was found
|
||||||
|
var webService *WebService
|
||||||
|
var route *Route
|
||||||
|
var err error
|
||||||
|
func() {
|
||||||
|
c.webServicesLock.RLock()
|
||||||
|
defer c.webServicesLock.RUnlock()
|
||||||
|
webService, route, err = c.router.SelectRoute(
|
||||||
|
c.webServices,
|
||||||
|
httpRequest)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// a non-200 response has already been written
|
||||||
|
// run container filters anyway ; they should not touch the response...
|
||||||
|
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
|
||||||
|
switch err.(type) {
|
||||||
|
case ServiceError:
|
||||||
|
ser := err.(ServiceError)
|
||||||
|
c.serviceErrorHandleFunc(ser, req, resp)
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
}}
|
||||||
|
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(writer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pathProcessor, routerProcessesPath := c.router.(PathProcessor)
|
||||||
|
if !routerProcessesPath {
|
||||||
|
pathProcessor = defaultPathProcessor{}
|
||||||
|
}
|
||||||
|
pathParams := pathProcessor.ExtractParameters(route, webService, httpRequest.URL.Path)
|
||||||
|
wrappedRequest, wrappedResponse := route.wrapRequestResponse(writer, httpRequest, pathParams)
|
||||||
|
// pass through filters (if any)
|
||||||
|
if len(c.containerFilters)+len(webService.filters)+len(route.Filters) > 0 {
|
||||||
|
// compose filter chain
|
||||||
|
allFilters := []FilterFunction{}
|
||||||
|
allFilters = append(allFilters, c.containerFilters...)
|
||||||
|
allFilters = append(allFilters, webService.filters...)
|
||||||
|
allFilters = append(allFilters, route.Filters...)
|
||||||
|
chain := FilterChain{Filters: allFilters, Target: func(req *Request, resp *Response) {
|
||||||
|
// handle request by route after passing all filters
|
||||||
|
route.Function(wrappedRequest, wrappedResponse)
|
||||||
|
}}
|
||||||
|
chain.ProcessFilter(wrappedRequest, wrappedResponse)
|
||||||
|
} else {
|
||||||
|
// no filters, handle request by route
|
||||||
|
route.Function(wrappedRequest, wrappedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixedPrefixPath returns the fixed part of the partspec ; it may include template vars {}
|
||||||
|
func fixedPrefixPath(pathspec string) string {
|
||||||
|
varBegin := strings.Index(pathspec, "{")
|
||||||
|
if -1 == varBegin {
|
||||||
|
return pathspec
|
||||||
|
}
|
||||||
|
return pathspec[:varBegin]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements net/http.Handler therefore a Container can be a Handler in a http.Server
|
||||||
|
func (c *Container) ServeHTTP(httpwriter http.ResponseWriter, httpRequest *http.Request) {
|
||||||
|
c.ServeMux.ServeHTTP(httpwriter, httpRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics.
|
||||||
|
func (c *Container) Handle(pattern string, handler http.Handler) {
|
||||||
|
c.ServeMux.Handle(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWithFilter registers the handler for the given pattern.
|
||||||
|
// Container's filter chain is applied for handler.
|
||||||
|
// If a handler already exists for pattern, HandleWithFilter panics.
|
||||||
|
func (c *Container) HandleWithFilter(pattern string, handler http.Handler) {
|
||||||
|
f := func(httpResponse http.ResponseWriter, httpRequest *http.Request) {
|
||||||
|
if len(c.containerFilters) == 0 {
|
||||||
|
handler.ServeHTTP(httpResponse, httpRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
|
||||||
|
handler.ServeHTTP(httpResponse, httpRequest)
|
||||||
|
}}
|
||||||
|
chain.ProcessFilter(NewRequest(httpRequest), NewResponse(httpResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Handle(pattern, http.HandlerFunc(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends a container FilterFunction. These are called before dispatching
|
||||||
|
// a http.Request to a WebService from the container
|
||||||
|
func (c *Container) Filter(filter FilterFunction) {
|
||||||
|
c.containerFilters = append(c.containerFilters, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisteredWebServices returns the collections of added WebServices
|
||||||
|
func (c *Container) RegisteredWebServices() []*WebService {
|
||||||
|
c.webServicesLock.RLock()
|
||||||
|
defer c.webServicesLock.RUnlock()
|
||||||
|
result := make([]*WebService, len(c.webServices))
|
||||||
|
for ix := range c.webServices {
|
||||||
|
result[ix] = c.webServices[ix]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeAllowedMethods returns a list of HTTP methods that are valid for a Request
|
||||||
|
func (c *Container) computeAllowedMethods(req *Request) []string {
|
||||||
|
// Go through all RegisteredWebServices() and all its Routes to collect the options
|
||||||
|
methods := []string{}
|
||||||
|
requestPath := req.Request.URL.Path
|
||||||
|
for _, ws := range c.RegisteredWebServices() {
|
||||||
|
matches := ws.pathExpr.Matcher.FindStringSubmatch(requestPath)
|
||||||
|
if matches != nil {
|
||||||
|
finalMatch := matches[len(matches)-1]
|
||||||
|
for _, rt := range ws.Routes() {
|
||||||
|
matches := rt.pathExpr.Matcher.FindStringSubmatch(finalMatch)
|
||||||
|
if matches != nil {
|
||||||
|
lastMatch := matches[len(matches)-1]
|
||||||
|
if lastMatch == "" || lastMatch == "/" { // do not include if value is neither empty nor ‘/’.
|
||||||
|
methods = append(methods, rt.Method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// methods = append(methods, "OPTIONS") not sure about this
|
||||||
|
return methods
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBasicRequestResponse creates a pair of Request,Response from its http versions.
|
||||||
|
// It is basic because no parameter or (produces) content-type information is given.
|
||||||
|
func newBasicRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request) (*Request, *Response) {
|
||||||
|
resp := NewResponse(httpWriter)
|
||||||
|
resp.requestAccept = httpRequest.Header.Get(HEADER_Accept)
|
||||||
|
return NewRequest(httpRequest), resp
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CrossOriginResourceSharing is used to create a Container Filter that implements CORS.
|
||||||
|
// Cross-origin resource sharing (CORS) is a mechanism that allows JavaScript on a web page
|
||||||
|
// to make XMLHttpRequests to another domain, not the domain the JavaScript originated from.
|
||||||
|
//
|
||||||
|
// http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
|
||||||
|
// http://enable-cors.org/server.html
|
||||||
|
// http://www.html5rocks.com/en/tutorials/cors/#toc-handling-a-not-so-simple-request
|
||||||
|
type CrossOriginResourceSharing struct {
|
||||||
|
ExposeHeaders []string // list of Header names
|
||||||
|
AllowedHeaders []string // list of Header names
|
||||||
|
AllowedDomains []string // list of allowed values for Http Origin. An allowed value can be a regular expression to support subdomain matching. If empty all are allowed.
|
||||||
|
AllowedMethods []string
|
||||||
|
MaxAge int // number of seconds before requiring new Options request
|
||||||
|
CookiesAllowed bool
|
||||||
|
Container *Container
|
||||||
|
|
||||||
|
allowedOriginPatterns []*regexp.Regexp // internal field for origin regexp check.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter is a filter function that implements the CORS flow as documented on http://enable-cors.org/server.html
|
||||||
|
// and http://www.html5rocks.com/static/images/cors_server_flowchart.png
|
||||||
|
func (c CrossOriginResourceSharing) Filter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
origin := req.Request.Header.Get(HEADER_Origin)
|
||||||
|
if len(origin) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Print("no Http header Origin set")
|
||||||
|
}
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.isOriginAllowed(origin) { // check whether this origin is allowed
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("HTTP Origin:%s is not part of %v, neither matches any part of %v", origin, c.AllowedDomains, c.allowedOriginPatterns)
|
||||||
|
}
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Request.Method != "OPTIONS" {
|
||||||
|
c.doActualRequest(req, resp)
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if acrm := req.Request.Header.Get(HEADER_AccessControlRequestMethod); acrm != "" {
|
||||||
|
c.doPreflightRequest(req, resp)
|
||||||
|
} else {
|
||||||
|
c.doActualRequest(req, resp)
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) doActualRequest(req *Request, resp *Response) {
|
||||||
|
c.setOptionsHeaders(req, resp)
|
||||||
|
// continue processing the response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CrossOriginResourceSharing) doPreflightRequest(req *Request, resp *Response) {
|
||||||
|
if len(c.AllowedMethods) == 0 {
|
||||||
|
if c.Container == nil {
|
||||||
|
c.AllowedMethods = DefaultContainer.computeAllowedMethods(req)
|
||||||
|
} else {
|
||||||
|
c.AllowedMethods = c.Container.computeAllowedMethods(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acrm := req.Request.Header.Get(HEADER_AccessControlRequestMethod)
|
||||||
|
if !c.isValidAccessControlRequestMethod(acrm, c.AllowedMethods) {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("Http header %s:%s is not in %v",
|
||||||
|
HEADER_AccessControlRequestMethod,
|
||||||
|
acrm,
|
||||||
|
c.AllowedMethods)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
acrhs := req.Request.Header.Get(HEADER_AccessControlRequestHeaders)
|
||||||
|
if len(acrhs) > 0 {
|
||||||
|
for _, each := range strings.Split(acrhs, ",") {
|
||||||
|
if !c.isValidAccessControlRequestHeader(strings.Trim(each, " ")) {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("Http header %s:%s is not in %v",
|
||||||
|
HEADER_AccessControlRequestHeaders,
|
||||||
|
acrhs,
|
||||||
|
c.AllowedHeaders)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowMethods, strings.Join(c.AllowedMethods, ","))
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowHeaders, acrhs)
|
||||||
|
c.setOptionsHeaders(req, resp)
|
||||||
|
|
||||||
|
// return http 200 response, no body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) setOptionsHeaders(req *Request, resp *Response) {
|
||||||
|
c.checkAndSetExposeHeaders(resp)
|
||||||
|
c.setAllowOriginHeader(req, resp)
|
||||||
|
c.checkAndSetAllowCredentials(resp)
|
||||||
|
if c.MaxAge > 0 {
|
||||||
|
resp.AddHeader(HEADER_AccessControlMaxAge, strconv.Itoa(c.MaxAge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) isOriginAllowed(origin string) bool {
|
||||||
|
if len(origin) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(c.AllowedDomains) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed := false
|
||||||
|
for _, domain := range c.AllowedDomains {
|
||||||
|
if domain == origin {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
if len(c.allowedOriginPatterns) == 0 {
|
||||||
|
// compile allowed domains to allowed origin patterns
|
||||||
|
allowedOriginRegexps, err := compileRegexps(c.AllowedDomains)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c.allowedOriginPatterns = allowedOriginRegexps
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range c.allowedOriginPatterns {
|
||||||
|
if allowed = pattern.MatchString(origin); allowed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) setAllowOriginHeader(req *Request, resp *Response) {
|
||||||
|
origin := req.Request.Header.Get(HEADER_Origin)
|
||||||
|
if c.isOriginAllowed(origin) {
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowOrigin, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) checkAndSetExposeHeaders(resp *Response) {
|
||||||
|
if len(c.ExposeHeaders) > 0 {
|
||||||
|
resp.AddHeader(HEADER_AccessControlExposeHeaders, strings.Join(c.ExposeHeaders, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) checkAndSetAllowCredentials(resp *Response) {
|
||||||
|
if c.CookiesAllowed {
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowCredentials, "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) isValidAccessControlRequestMethod(method string, allowedMethods []string) bool {
|
||||||
|
for _, each := range allowedMethods {
|
||||||
|
if each == method {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CrossOriginResourceSharing) isValidAccessControlRequestHeader(header string) bool {
|
||||||
|
for _, each := range c.AllowedHeaders {
|
||||||
|
if strings.ToLower(each) == strings.ToLower(header) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a list of strings and compile them into a list of regular expressions.
|
||||||
|
func compileRegexps(regexpStrings []string) ([]*regexp.Regexp, error) {
|
||||||
|
regexps := []*regexp.Regexp{}
|
||||||
|
for _, regexpStr := range regexpStrings {
|
||||||
|
r, err := regexp.Compile(regexpStr)
|
||||||
|
if err != nil {
|
||||||
|
return regexps, err
|
||||||
|
}
|
||||||
|
regexps = append(regexps, r)
|
||||||
|
}
|
||||||
|
return regexps, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets.
|
||||||
|
type CurlyRouter struct{}
|
||||||
|
|
||||||
|
// SelectRoute is part of the Router interface and returns the best match
|
||||||
|
// for the WebService and its Route for the given Request.
|
||||||
|
func (c CurlyRouter) SelectRoute(
|
||||||
|
webServices []*WebService,
|
||||||
|
httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {
|
||||||
|
|
||||||
|
requestTokens := tokenizePath(httpRequest.URL.Path)
|
||||||
|
|
||||||
|
detectedService := c.detectWebService(requestTokens, webServices)
|
||||||
|
if detectedService == nil {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no WebService was found to match URL path:%s\n", httpRequest.URL.Path)
|
||||||
|
}
|
||||||
|
return nil, nil, NewError(http.StatusNotFound, "404: Page Not Found")
|
||||||
|
}
|
||||||
|
candidateRoutes := c.selectRoutes(detectedService, requestTokens)
|
||||||
|
if len(candidateRoutes) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route in WebService with path %s was found to match URL path:%s\n", detectedService.rootPath, httpRequest.URL.Path)
|
||||||
|
}
|
||||||
|
return detectedService, nil, NewError(http.StatusNotFound, "404: Page Not Found")
|
||||||
|
}
|
||||||
|
selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
|
||||||
|
if selectedRoute == nil {
|
||||||
|
return detectedService, nil, err
|
||||||
|
}
|
||||||
|
return detectedService, selectedRoute, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
|
||||||
|
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
|
||||||
|
candidates := sortableCurlyRoutes{}
|
||||||
|
for _, each := range ws.routes {
|
||||||
|
matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens)
|
||||||
|
if matches {
|
||||||
|
candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(candidates))
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
|
||||||
|
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string) (matches bool, paramCount int, staticCount int) {
|
||||||
|
if len(routeTokens) < len(requestTokens) {
|
||||||
|
// proceed in matching only if last routeToken is wildcard
|
||||||
|
count := len(routeTokens)
|
||||||
|
if count == 0 || !strings.HasSuffix(routeTokens[count-1], "*}") {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
// proceed
|
||||||
|
}
|
||||||
|
for i, routeToken := range routeTokens {
|
||||||
|
if i == len(requestTokens) {
|
||||||
|
// reached end of request path
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
requestToken := requestTokens[i]
|
||||||
|
if strings.HasPrefix(routeToken, "{") {
|
||||||
|
paramCount++
|
||||||
|
if colon := strings.Index(routeToken, ":"); colon != -1 {
|
||||||
|
// match by regex
|
||||||
|
matchesToken, matchesRemainder := c.regularMatchesPathToken(routeToken, colon, requestToken)
|
||||||
|
if !matchesToken {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
if matchesRemainder {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // no { prefix
|
||||||
|
if requestToken != routeToken {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
staticCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, paramCount, staticCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// regularMatchesPathToken tests whether the regular expression part of routeToken matches the requestToken or all remaining tokens
|
||||||
|
// format routeToken is {someVar:someExpression}, e.g. {zipcode:[\d][\d][\d][\d][A-Z][A-Z]}
|
||||||
|
func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, requestToken string) (matchesToken bool, matchesRemainder bool) {
|
||||||
|
regPart := routeToken[colon+1 : len(routeToken)-1]
|
||||||
|
if regPart == "*" {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("wildcard parameter detected in route token %s that matches %s\n", routeToken, requestToken)
|
||||||
|
}
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
matched, err := regexp.MatchString(regPart, requestToken)
|
||||||
|
return (matched && err == nil), false
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsr311Router = RouterJSR311{}
|
||||||
|
|
||||||
|
// detectRoute selectes from a list of Route the first match by inspecting both the Accept and Content-Type
|
||||||
|
// headers of the Request. See also RouterJSR311 in jsr311.go
|
||||||
|
func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
|
||||||
|
// tracing is done inside detectRoute
|
||||||
|
return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectWebService returns the best matching webService given the list of path tokens.
|
||||||
|
// see also computeWebserviceScore
|
||||||
|
func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
|
||||||
|
var best *WebService
|
||||||
|
score := -1
|
||||||
|
for _, each := range webServices {
|
||||||
|
matches, eachScore := c.computeWebserviceScore(requestTokens, each.pathExpr.tokens)
|
||||||
|
if matches && (eachScore > score) {
|
||||||
|
best = each
|
||||||
|
score = eachScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeWebserviceScore returns whether tokens match and
|
||||||
|
// the weighted score of the longest matching consecutive tokens from the beginning.
|
||||||
|
func (c CurlyRouter) computeWebserviceScore(requestTokens []string, tokens []string) (bool, int) {
|
||||||
|
if len(tokens) > len(requestTokens) {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
score := 0
|
||||||
|
for i := 0; i < len(tokens); i++ {
|
||||||
|
each := requestTokens[i]
|
||||||
|
other := tokens[i]
|
||||||
|
if len(each) == 0 && len(other) == 0 {
|
||||||
|
score++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(other) > 0 && strings.HasPrefix(other, "{") {
|
||||||
|
// no empty match
|
||||||
|
if len(each) == 0 {
|
||||||
|
return false, score
|
||||||
|
}
|
||||||
|
score += 1
|
||||||
|
} else {
|
||||||
|
// not a parameter
|
||||||
|
if each != other {
|
||||||
|
return false, score
|
||||||
|
}
|
||||||
|
score += (len(tokens) - i) * 10 //fuzzy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, score
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// curlyRoute exits for sorting Routes by the CurlyRouter based on number of parameters and number of static path elements.
|
||||||
|
type curlyRoute struct {
|
||||||
|
route Route
|
||||||
|
paramCount int
|
||||||
|
staticCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableCurlyRoutes []curlyRoute
|
||||||
|
|
||||||
|
func (s *sortableCurlyRoutes) add(route curlyRoute) {
|
||||||
|
*s = append(*s, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sortableCurlyRoutes) routes() (routes []Route) {
|
||||||
|
for _, each := range s {
|
||||||
|
routes = append(routes, each.route) // TODO change return type
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sortableCurlyRoutes) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
func (s sortableCurlyRoutes) Swap(i, j int) {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
func (s sortableCurlyRoutes) Less(i, j int) bool {
|
||||||
|
ci := s[i]
|
||||||
|
cj := s[j]
|
||||||
|
|
||||||
|
// primary key
|
||||||
|
if ci.staticCount < cj.staticCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.staticCount > cj.staticCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// secundary key
|
||||||
|
if ci.paramCount < cj.paramCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.paramCount > cj.paramCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ci.route.Path < cj.route.Path
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
Package restful , a lean package for creating REST-style WebServices without magic.
|
||||||
|
|
||||||
|
WebServices and Routes
|
||||||
|
|
||||||
|
A WebService has a collection of Route objects that dispatch incoming Http Requests to a function calls.
|
||||||
|
Typically, a WebService has a root path (e.g. /users) and defines common MIME types for its routes.
|
||||||
|
WebServices must be added to a container (see below) in order to handler Http requests from a server.
|
||||||
|
|
||||||
|
A Route is defined by a HTTP method, an URL path and (optionally) the MIME types it consumes (Content-Type) and produces (Accept).
|
||||||
|
This package has the logic to find the best matching Route and if found, call its Function.
|
||||||
|
|
||||||
|
ws := new(restful.WebService)
|
||||||
|
ws.
|
||||||
|
Path("/users").
|
||||||
|
Consumes(restful.MIME_JSON, restful.MIME_XML).
|
||||||
|
Produces(restful.MIME_JSON, restful.MIME_XML)
|
||||||
|
|
||||||
|
ws.Route(ws.GET("/{user-id}").To(u.findUser)) // u is a UserResource
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
// GET http://localhost:8080/users/1
|
||||||
|
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
|
||||||
|
id := request.PathParameter("user-id")
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
The (*Request, *Response) arguments provide functions for reading information from the request and writing information back to the response.
|
||||||
|
|
||||||
|
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-user-resource.go with a full implementation.
|
||||||
|
|
||||||
|
Regular expression matching Routes
|
||||||
|
|
||||||
|
A Route parameter can be specified using the format "uri/{var[:regexp]}" or the special version "uri/{var:*}" for matching the tail of the path.
|
||||||
|
For example, /persons/{name:[A-Z][A-Z]} can be used to restrict values for the parameter "name" to only contain capital alphabetic characters.
|
||||||
|
Regular expressions must use the standard Go syntax as described in the regexp package. (https://code.google.com/p/re2/wiki/Syntax)
|
||||||
|
This feature requires the use of a CurlyRouter.
|
||||||
|
|
||||||
|
Containers
|
||||||
|
|
||||||
|
A Container holds a collection of WebServices, Filters and a http.ServeMux for multiplexing http requests.
|
||||||
|
Using the statements "restful.Add(...) and restful.Filter(...)" will register WebServices and Filters to the Default Container.
|
||||||
|
The Default container of go-restful uses the http.DefaultServeMux.
|
||||||
|
You can create your own Container and create a new http.Server for that particular container.
|
||||||
|
|
||||||
|
container := restful.NewContainer()
|
||||||
|
server := &http.Server{Addr: ":8081", Handler: container}
|
||||||
|
|
||||||
|
Filters
|
||||||
|
|
||||||
|
A filter dynamically intercepts requests and responses to transform or use the information contained in the requests or responses.
|
||||||
|
You can use filters to perform generic logging, measurement, authentication, redirect, set response headers etc.
|
||||||
|
In the restful package there are three hooks into the request,response flow where filters can be added.
|
||||||
|
Each filter must define a FilterFunction:
|
||||||
|
|
||||||
|
func (req *restful.Request, resp *restful.Response, chain *restful.FilterChain)
|
||||||
|
|
||||||
|
Use the following statement to pass the request,response pair to the next filter or RouteFunction
|
||||||
|
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
|
||||||
|
Container Filters
|
||||||
|
|
||||||
|
These are processed before any registered WebService.
|
||||||
|
|
||||||
|
// install a (global) filter for the default container (processed before any webservice)
|
||||||
|
restful.Filter(globalLogging)
|
||||||
|
|
||||||
|
WebService Filters
|
||||||
|
|
||||||
|
These are processed before any Route of a WebService.
|
||||||
|
|
||||||
|
// install a webservice filter (processed before any route)
|
||||||
|
ws.Filter(webserviceLogging).Filter(measureTime)
|
||||||
|
|
||||||
|
|
||||||
|
Route Filters
|
||||||
|
|
||||||
|
These are processed before calling the function associated with the Route.
|
||||||
|
|
||||||
|
// install 2 chained route filters (processed before calling findUser)
|
||||||
|
ws.Route(ws.GET("/{user-id}").Filter(routeLogging).Filter(NewCountFilter().routeCounter).To(findUser))
|
||||||
|
|
||||||
|
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-filters.go with full implementations.
|
||||||
|
|
||||||
|
Response Encoding
|
||||||
|
|
||||||
|
Two encodings are supported: gzip and deflate. To enable this for all responses:
|
||||||
|
|
||||||
|
restful.DefaultContainer.EnableContentEncoding(true)
|
||||||
|
|
||||||
|
If a Http request includes the Accept-Encoding header then the response content will be compressed using the specified encoding.
|
||||||
|
Alternatively, you can create a Filter that performs the encoding and install it per WebService or Route.
|
||||||
|
|
||||||
|
See the example https://github.com/emicklei/go-restful/blob/master/examples/restful-encoding-filter.go
|
||||||
|
|
||||||
|
OPTIONS support
|
||||||
|
|
||||||
|
By installing a pre-defined container filter, your Webservice(s) can respond to the OPTIONS Http request.
|
||||||
|
|
||||||
|
Filter(OPTIONSFilter())
|
||||||
|
|
||||||
|
CORS
|
||||||
|
|
||||||
|
By installing the filter of a CrossOriginResourceSharing (CORS), your WebService(s) can handle CORS requests.
|
||||||
|
|
||||||
|
cors := CrossOriginResourceSharing{ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer}
|
||||||
|
Filter(cors.Filter)
|
||||||
|
|
||||||
|
Error Handling
|
||||||
|
|
||||||
|
Unexpected things happen. If a request cannot be processed because of a failure, your service needs to tell via the response what happened and why.
|
||||||
|
For this reason HTTP status codes exist and it is important to use the correct code in every exceptional situation.
|
||||||
|
|
||||||
|
400: Bad Request
|
||||||
|
|
||||||
|
If path or query parameters are not valid (content or type) then use http.StatusBadRequest.
|
||||||
|
|
||||||
|
404: Not Found
|
||||||
|
|
||||||
|
Despite a valid URI, the resource requested may not be available
|
||||||
|
|
||||||
|
500: Internal Server Error
|
||||||
|
|
||||||
|
If the application logic could not process the request (or write the response) then use http.StatusInternalServerError.
|
||||||
|
|
||||||
|
405: Method Not Allowed
|
||||||
|
|
||||||
|
The request has a valid URL but the method (GET,PUT,POST,...) is not allowed.
|
||||||
|
|
||||||
|
406: Not Acceptable
|
||||||
|
|
||||||
|
The request does not have or has an unknown Accept Header set for this operation.
|
||||||
|
|
||||||
|
415: Unsupported Media Type
|
||||||
|
|
||||||
|
The request does not have or has an unknown Content-Type Header set for this operation.
|
||||||
|
|
||||||
|
ServiceError
|
||||||
|
|
||||||
|
In addition to setting the correct (error) Http status code, you can choose to write a ServiceError message on the response.
|
||||||
|
|
||||||
|
Performance options
|
||||||
|
|
||||||
|
This package has several options that affect the performance of your service. It is important to understand them and how you can change it.
|
||||||
|
|
||||||
|
restful.DefaultContainer.DoNotRecover(false)
|
||||||
|
|
||||||
|
DoNotRecover controls whether panics will be caught to return HTTP 500.
|
||||||
|
If set to false, the container will recover from panics.
|
||||||
|
Default value is true
|
||||||
|
|
||||||
|
restful.SetCompressorProvider(NewBoundedCachedCompressors(20, 20))
|
||||||
|
|
||||||
|
If content encoding is enabled then the default strategy for getting new gzip/zlib writers and readers is to use a sync.Pool.
|
||||||
|
Because writers are expensive structures, performance is even more improved when using a preloaded cache. You can also inject your own implementation.
|
||||||
|
|
||||||
|
Trouble shooting
|
||||||
|
|
||||||
|
This package has the means to produce detail logging of the complete Http request matching process and filter invocation.
|
||||||
|
Enabling this feature requires you to set an implementation of restful.StdLogger (e.g. log.Logger) instance such as:
|
||||||
|
|
||||||
|
restful.TraceLogger(log.New(os.Stdout, "[restful] ", log.LstdFlags|log.Lshortfile))
|
||||||
|
|
||||||
|
Logging
|
||||||
|
|
||||||
|
The restful.SetLogger() method allows you to override the logger used by the package. By default restful
|
||||||
|
uses the standard library `log` package and logs to stdout. Different logging packages are supported as
|
||||||
|
long as they conform to `StdLogger` interface defined in the `log` sub-package, writing an adapter for your
|
||||||
|
preferred package is simple.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
|
||||||
|
[project]: https://github.com/emicklei/go-restful
|
||||||
|
|
||||||
|
[examples]: https://github.com/emicklei/go-restful/blob/master/examples
|
||||||
|
|
||||||
|
[design]: http://ernestmicklei.com/2012/11/11/go-restful-api-design/
|
||||||
|
|
||||||
|
[showcases]: https://github.com/emicklei/mora, https://github.com/emicklei/landskape
|
||||||
|
|
||||||
|
(c) 2012-2015, http://ernestmicklei.com. MIT License
|
||||||
|
*/
|
||||||
|
package restful
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EntityReaderWriter can read and write values using an encoding such as JSON,XML.
|
||||||
|
type EntityReaderWriter interface {
|
||||||
|
// Read a serialized version of the value from the request.
|
||||||
|
// The Request may have a decompressing reader. Depends on Content-Encoding.
|
||||||
|
Read(req *Request, v interface{}) error
|
||||||
|
|
||||||
|
// Write a serialized version of the value on the response.
|
||||||
|
// The Response may have a compressing writer. Depends on Accept-Encoding.
|
||||||
|
// status should be a valid Http Status code
|
||||||
|
Write(resp *Response, status int, v interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// entityAccessRegistry is a singleton
|
||||||
|
var entityAccessRegistry = &entityReaderWriters{
|
||||||
|
protection: new(sync.RWMutex),
|
||||||
|
accessors: map[string]EntityReaderWriter{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// entityReaderWriters associates MIME to an EntityReaderWriter
|
||||||
|
type entityReaderWriters struct {
|
||||||
|
protection *sync.RWMutex
|
||||||
|
accessors map[string]EntityReaderWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterEntityAccessor(MIME_JSON, NewEntityAccessorJSON(MIME_JSON))
|
||||||
|
RegisterEntityAccessor(MIME_XML, NewEntityAccessorXML(MIME_XML))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEntityAccessor add/overrides the ReaderWriter for encoding content with this MIME type.
|
||||||
|
func RegisterEntityAccessor(mime string, erw EntityReaderWriter) {
|
||||||
|
entityAccessRegistry.protection.Lock()
|
||||||
|
defer entityAccessRegistry.protection.Unlock()
|
||||||
|
entityAccessRegistry.accessors[mime] = erw
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEntityAccessorJSON returns a new EntityReaderWriter for accessing JSON content.
|
||||||
|
// This package is already initialized with such an accessor using the MIME_JSON contentType.
|
||||||
|
func NewEntityAccessorJSON(contentType string) EntityReaderWriter {
|
||||||
|
return entityJSONAccess{ContentType: contentType}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEntityAccessorXML returns a new EntityReaderWriter for accessing XML content.
|
||||||
|
// This package is already initialized with such an accessor using the MIME_XML contentType.
|
||||||
|
func NewEntityAccessorXML(contentType string) EntityReaderWriter {
|
||||||
|
return entityXMLAccess{ContentType: contentType}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessorAt returns the registered ReaderWriter for this MIME type.
|
||||||
|
func (r *entityReaderWriters) accessorAt(mime string) (EntityReaderWriter, bool) {
|
||||||
|
r.protection.RLock()
|
||||||
|
defer r.protection.RUnlock()
|
||||||
|
er, ok := r.accessors[mime]
|
||||||
|
if !ok {
|
||||||
|
// retry with reverse lookup
|
||||||
|
// more expensive but we are in an exceptional situation anyway
|
||||||
|
for k, v := range r.accessors {
|
||||||
|
if strings.Contains(mime, k) {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return er, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// entityXMLAccess is a EntityReaderWriter for XML encoding
|
||||||
|
type entityXMLAccess struct {
|
||||||
|
// This is used for setting the Content-Type header when writing
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read unmarshalls the value from XML
|
||||||
|
func (e entityXMLAccess) Read(req *Request, v interface{}) error {
|
||||||
|
return xml.NewDecoder(req.Request.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write marshalls the value to JSON and set the Content-Type Header.
|
||||||
|
func (e entityXMLAccess) Write(resp *Response, status int, v interface{}) error {
|
||||||
|
return writeXML(resp, status, e.ContentType, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeXML marshalls the value to JSON and set the Content-Type Header.
|
||||||
|
func writeXML(resp *Response, status int, contentType string, v interface{}) error {
|
||||||
|
if v == nil {
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
// do not write a nil representation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.prettyPrint {
|
||||||
|
// pretty output must be created and written explicitly
|
||||||
|
output, err := xml.MarshalIndent(v, " ", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp.Header().Set(HEADER_ContentType, contentType)
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
_, err = resp.Write([]byte(xml.Header))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = resp.Write(output)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// not-so-pretty
|
||||||
|
resp.Header().Set(HEADER_ContentType, contentType)
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
return xml.NewEncoder(resp).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// entityJSONAccess is a EntityReaderWriter for JSON encoding
|
||||||
|
type entityJSONAccess struct {
|
||||||
|
// This is used for setting the Content-Type header when writing
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read unmarshalls the value from JSON
|
||||||
|
func (e entityJSONAccess) Read(req *Request, v interface{}) error {
|
||||||
|
decoder := NewDecoder(req.Request.Body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
return decoder.Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write marshalls the value to JSON and set the Content-Type Header.
|
||||||
|
func (e entityJSONAccess) Write(resp *Response, status int, v interface{}) error {
|
||||||
|
return writeJSON(resp, status, e.ContentType, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write marshalls the value to JSON and set the Content-Type Header.
|
||||||
|
func writeJSON(resp *Response, status int, contentType string, v interface{}) error {
|
||||||
|
if v == nil {
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
// do not write a nil representation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.prettyPrint {
|
||||||
|
// pretty output must be created and written explicitly
|
||||||
|
output, err := MarshalIndent(v, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp.Header().Set(HEADER_ContentType, contentType)
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
_, err = resp.Write(output)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// not-so-pretty
|
||||||
|
resp.Header().Set(HEADER_ContentType, contentType)
|
||||||
|
resp.WriteHeader(status)
|
||||||
|
return NewEncoder(resp).Encode(v)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// FilterChain is a request scoped object to process one or more filters before calling the target RouteFunction.
|
||||||
|
type FilterChain struct {
|
||||||
|
Filters []FilterFunction // ordered list of FilterFunction
|
||||||
|
Index int // index into filters that is currently in progress
|
||||||
|
Target RouteFunction // function to call after passing all filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessFilter passes the request,response pair through the next of Filters.
|
||||||
|
// Each filter can decide to proceed to the next Filter or handle the Response itself.
|
||||||
|
func (f *FilterChain) ProcessFilter(request *Request, response *Response) {
|
||||||
|
if f.Index < len(f.Filters) {
|
||||||
|
f.Index++
|
||||||
|
f.Filters[f.Index-1](request, response, f)
|
||||||
|
} else {
|
||||||
|
f.Target(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterFunction definitions must call ProcessFilter on the FilterChain to pass on the control and eventually call the RouteFunction
|
||||||
|
type FilterFunction func(*Request, *Response, *FilterChain)
|
||||||
|
|
||||||
|
// NoBrowserCacheFilter is a filter function to set HTTP headers that disable browser caching
|
||||||
|
// See examples/restful-no-cache-filter.go for usage
|
||||||
|
func NoBrowserCacheFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
resp.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // HTTP 1.1.
|
||||||
|
resp.Header().Set("Pragma", "no-cache") // HTTP 1.0.
|
||||||
|
resp.Header().Set("Expires", "0") // Proxies.
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !jsoniter
|
||||||
|
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
var (
|
||||||
|
MarshalIndent = json.MarshalIndent
|
||||||
|
NewDecoder = json.NewDecoder
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// +build jsoniter
|
||||||
|
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
var (
|
||||||
|
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
MarshalIndent = json.MarshalIndent
|
||||||
|
NewDecoder = json.NewDecoder
|
||||||
|
NewEncoder = json.NewEncoder
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouterJSR311 implements the flow for matching Requests to Routes (and consequently Resource Functions)
|
||||||
|
// as specified by the JSR311 http://jsr311.java.net/nonav/releases/1.1/spec/spec.html.
|
||||||
|
// RouterJSR311 implements the Router interface.
|
||||||
|
// Concept of locators is not implemented.
|
||||||
|
type RouterJSR311 struct{}
|
||||||
|
|
||||||
|
// SelectRoute is part of the Router interface and returns the best match
|
||||||
|
// for the WebService and its Route for the given Request.
|
||||||
|
func (r RouterJSR311) SelectRoute(
|
||||||
|
webServices []*WebService,
|
||||||
|
httpRequest *http.Request) (selectedService *WebService, selectedRoute *Route, err error) {
|
||||||
|
|
||||||
|
// Identify the root resource class (WebService)
|
||||||
|
dispatcher, finalMatch, err := r.detectDispatcher(httpRequest.URL.Path, webServices)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewError(http.StatusNotFound, "")
|
||||||
|
}
|
||||||
|
// Obtain the set of candidate methods (Routes)
|
||||||
|
routes := r.selectRoutes(dispatcher, finalMatch)
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return dispatcher, nil, NewError(http.StatusNotFound, "404: Page Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify the method (Route) that will handle the request
|
||||||
|
route, ok := r.detectRoute(routes, httpRequest)
|
||||||
|
return dispatcher, route, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractParameters is used to obtain the path parameters from the route using the same matching
|
||||||
|
// engine as the JSR 311 router.
|
||||||
|
func (r RouterJSR311) ExtractParameters(route *Route, webService *WebService, urlPath string) map[string]string {
|
||||||
|
webServiceExpr := webService.pathExpr
|
||||||
|
webServiceMatches := webServiceExpr.Matcher.FindStringSubmatch(urlPath)
|
||||||
|
pathParameters := r.extractParams(webServiceExpr, webServiceMatches)
|
||||||
|
routeExpr := route.pathExpr
|
||||||
|
routeMatches := routeExpr.Matcher.FindStringSubmatch(webServiceMatches[len(webServiceMatches)-1])
|
||||||
|
routeParams := r.extractParams(routeExpr, routeMatches)
|
||||||
|
for key, value := range routeParams {
|
||||||
|
pathParameters[key] = value
|
||||||
|
}
|
||||||
|
return pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RouterJSR311) extractParams(pathExpr *pathExpression, matches []string) map[string]string {
|
||||||
|
params := map[string]string{}
|
||||||
|
for i := 1; i < len(matches); i++ {
|
||||||
|
if len(pathExpr.VarNames) >= i {
|
||||||
|
params[pathExpr.VarNames[i-1]] = matches[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2
|
||||||
|
func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*Route, error) {
|
||||||
|
ifOk := []Route{}
|
||||||
|
for _, each := range routes {
|
||||||
|
ok := true
|
||||||
|
for _, fn := range each.If {
|
||||||
|
if !fn(httpRequest) {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
ifOk = append(ifOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ifOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (from %d) that passes conditional checks", len(routes))
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusNotFound, "404: Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// http method
|
||||||
|
methodOk := []Route{}
|
||||||
|
for _, each := range ifOk {
|
||||||
|
if httpRequest.Method == each.Method {
|
||||||
|
methodOk = append(methodOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(methodOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (in %d routes) that matches HTTP method %s\n", len(routes), httpRequest.Method)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusMethodNotAllowed, "405: Method Not Allowed")
|
||||||
|
}
|
||||||
|
inputMediaOk := methodOk
|
||||||
|
|
||||||
|
// content-type
|
||||||
|
contentType := httpRequest.Header.Get(HEADER_ContentType)
|
||||||
|
inputMediaOk = []Route{}
|
||||||
|
for _, each := range methodOk {
|
||||||
|
if each.matchesContentType(contentType) {
|
||||||
|
inputMediaOk = append(inputMediaOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(inputMediaOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (from %d) that matches HTTP Content-Type: %s\n", len(methodOk), contentType)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusUnsupportedMediaType, "415: Unsupported Media Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept
|
||||||
|
outputMediaOk := []Route{}
|
||||||
|
accept := httpRequest.Header.Get(HEADER_Accept)
|
||||||
|
if len(accept) == 0 {
|
||||||
|
accept = "*/*"
|
||||||
|
}
|
||||||
|
for _, each := range inputMediaOk {
|
||||||
|
if each.matchesAccept(accept) {
|
||||||
|
outputMediaOk = append(outputMediaOk, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(outputMediaOk) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no Route found (from %d) that matches HTTP Accept: %s\n", len(inputMediaOk), accept)
|
||||||
|
}
|
||||||
|
return nil, NewError(http.StatusNotAcceptable, "406: Not Acceptable")
|
||||||
|
}
|
||||||
|
// return r.bestMatchByMedia(outputMediaOk, contentType, accept), nil
|
||||||
|
return &outputMediaOk[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2
|
||||||
|
// n/m > n/* > */*
|
||||||
|
func (r RouterJSR311) bestMatchByMedia(routes []Route, contentType string, accept string) *Route {
|
||||||
|
// TODO
|
||||||
|
return &routes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2 (step 2)
|
||||||
|
func (r RouterJSR311) selectRoutes(dispatcher *WebService, pathRemainder string) []Route {
|
||||||
|
filtered := &sortableRouteCandidates{}
|
||||||
|
for _, each := range dispatcher.Routes() {
|
||||||
|
pathExpr := each.pathExpr
|
||||||
|
matches := pathExpr.Matcher.FindStringSubmatch(pathRemainder)
|
||||||
|
if matches != nil {
|
||||||
|
lastMatch := matches[len(matches)-1]
|
||||||
|
if len(lastMatch) == 0 || lastMatch == "/" { // do not include if value is neither empty nor ‘/’.
|
||||||
|
filtered.candidates = append(filtered.candidates,
|
||||||
|
routeCandidate{each, len(matches) - 1, pathExpr.LiteralCount, pathExpr.VarCount})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered.candidates) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("WebService on path %s has no routes that match URL path remainder:%s\n", dispatcher.rootPath, pathRemainder)
|
||||||
|
}
|
||||||
|
return []Route{}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(filtered))
|
||||||
|
|
||||||
|
// select other routes from candidates whoes expression matches rmatch
|
||||||
|
matchingRoutes := []Route{filtered.candidates[0].route}
|
||||||
|
for c := 1; c < len(filtered.candidates); c++ {
|
||||||
|
each := filtered.candidates[c]
|
||||||
|
if each.route.pathExpr.Matcher.MatchString(pathRemainder) {
|
||||||
|
matchingRoutes = append(matchingRoutes, each.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchingRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-360003.7.2 (step 1)
|
||||||
|
func (r RouterJSR311) detectDispatcher(requestPath string, dispatchers []*WebService) (*WebService, string, error) {
|
||||||
|
filtered := &sortableDispatcherCandidates{}
|
||||||
|
for _, each := range dispatchers {
|
||||||
|
matches := each.pathExpr.Matcher.FindStringSubmatch(requestPath)
|
||||||
|
if matches != nil {
|
||||||
|
filtered.candidates = append(filtered.candidates,
|
||||||
|
dispatcherCandidate{each, matches[len(matches)-1], len(matches), each.pathExpr.LiteralCount, each.pathExpr.VarCount})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered.candidates) == 0 {
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no WebService was found to match URL path:%s\n", requestPath)
|
||||||
|
}
|
||||||
|
return nil, "", errors.New("not found")
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(filtered))
|
||||||
|
return filtered.candidates[0].dispatcher, filtered.candidates[0].finalMatch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types and functions to support the sorting of Routes
|
||||||
|
|
||||||
|
type routeCandidate struct {
|
||||||
|
route Route
|
||||||
|
matchesCount int // the number of capturing groups
|
||||||
|
literalCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
nonDefaultCount int // the number of capturing groups with non-default regular expressions (i.e. not ‘([^ /]+?)’)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeCandidate) expressionToMatch() string {
|
||||||
|
return r.route.pathExpr.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeCandidate) String() string {
|
||||||
|
return fmt.Sprintf("(m=%d,l=%d,n=%d)", r.matchesCount, r.literalCount, r.nonDefaultCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableRouteCandidates struct {
|
||||||
|
candidates []routeCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rcs *sortableRouteCandidates) Len() int {
|
||||||
|
return len(rcs.candidates)
|
||||||
|
}
|
||||||
|
func (rcs *sortableRouteCandidates) Swap(i, j int) {
|
||||||
|
rcs.candidates[i], rcs.candidates[j] = rcs.candidates[j], rcs.candidates[i]
|
||||||
|
}
|
||||||
|
func (rcs *sortableRouteCandidates) Less(i, j int) bool {
|
||||||
|
ci := rcs.candidates[i]
|
||||||
|
cj := rcs.candidates[j]
|
||||||
|
// primary key
|
||||||
|
if ci.literalCount < cj.literalCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.literalCount > cj.literalCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// secundary key
|
||||||
|
if ci.matchesCount < cj.matchesCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.matchesCount > cj.matchesCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// tertiary key
|
||||||
|
if ci.nonDefaultCount < cj.nonDefaultCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.nonDefaultCount > cj.nonDefaultCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// quaternary key ("source" is interpreted as Path)
|
||||||
|
return ci.route.Path < cj.route.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types and functions to support the sorting of Dispatchers
|
||||||
|
|
||||||
|
type dispatcherCandidate struct {
|
||||||
|
dispatcher *WebService
|
||||||
|
finalMatch string
|
||||||
|
matchesCount int // the number of capturing groups
|
||||||
|
literalCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
nonDefaultCount int // the number of capturing groups with non-default regular expressions (i.e. not ‘([^ /]+?)’)
|
||||||
|
}
|
||||||
|
type sortableDispatcherCandidates struct {
|
||||||
|
candidates []dispatcherCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *sortableDispatcherCandidates) Len() int {
|
||||||
|
return len(dc.candidates)
|
||||||
|
}
|
||||||
|
func (dc *sortableDispatcherCandidates) Swap(i, j int) {
|
||||||
|
dc.candidates[i], dc.candidates[j] = dc.candidates[j], dc.candidates[i]
|
||||||
|
}
|
||||||
|
func (dc *sortableDispatcherCandidates) Less(i, j int) bool {
|
||||||
|
ci := dc.candidates[i]
|
||||||
|
cj := dc.candidates[j]
|
||||||
|
// primary key
|
||||||
|
if ci.matchesCount < cj.matchesCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.matchesCount > cj.matchesCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// secundary key
|
||||||
|
if ci.literalCount < cj.literalCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ci.literalCount > cj.literalCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// tertiary key
|
||||||
|
return ci.nonDefaultCount < cj.nonDefaultCount
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdlog "log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StdLogger corresponds to a minimal subset of the interface satisfied by stdlib log.Logger
|
||||||
|
type StdLogger interface {
|
||||||
|
Print(v ...interface{})
|
||||||
|
Printf(format string, v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var Logger StdLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// default Logger
|
||||||
|
SetLogger(stdlog.New(os.Stderr, "[restful] ", stdlog.LstdFlags|stdlog.Lshortfile))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger sets the logger for this package
|
||||||
|
func SetLogger(customLogger StdLogger) {
|
||||||
|
Logger = customLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print delegates to the Logger
|
||||||
|
func Print(v ...interface{}) {
|
||||||
|
Logger.Print(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf delegates to the Logger
|
||||||
|
func Printf(format string, v ...interface{}) {
|
||||||
|
Logger.Printf(format, v...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2014 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
import (
|
||||||
|
"github.com/emicklei/go-restful/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var trace bool = false
|
||||||
|
var traceLogger log.StdLogger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
traceLogger = log.Logger // use the package logger by default
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceLogger enables detailed logging of Http request matching and filter invocation. Default no logger is set.
|
||||||
|
// You may call EnableTracing() directly to enable trace logging to the package-wide logger.
|
||||||
|
func TraceLogger(logger log.StdLogger) {
|
||||||
|
traceLogger = logger
|
||||||
|
EnableTracing(logger != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger exposes the setter for the global logger on the top-level package
|
||||||
|
func SetLogger(customLogger log.StdLogger) {
|
||||||
|
log.SetLogger(customLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableTracing can be used to Trace logging on and off.
|
||||||
|
func EnableTracing(enabled bool) {
|
||||||
|
trace = enabled
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mime struct {
|
||||||
|
media string
|
||||||
|
quality float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertMime adds a mime to a list and keeps it sorted by quality.
|
||||||
|
func insertMime(l []mime, e mime) []mime {
|
||||||
|
for i, each := range l {
|
||||||
|
// if current mime has lower quality then insert before
|
||||||
|
if e.quality > each.quality {
|
||||||
|
left := append([]mime{}, l[0:i]...)
|
||||||
|
return append(append(left, e), l[i:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(l, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortedMimes returns a list of mime sorted (desc) by its specified quality.
|
||||||
|
func sortedMimes(accept string) (sorted []mime) {
|
||||||
|
for _, each := range strings.Split(accept, ",") {
|
||||||
|
typeAndQuality := strings.Split(strings.Trim(each, " "), ";")
|
||||||
|
if len(typeAndQuality) == 1 {
|
||||||
|
sorted = insertMime(sorted, mime{typeAndQuality[0], 1.0})
|
||||||
|
} else {
|
||||||
|
// take factor
|
||||||
|
parts := strings.Split(typeAndQuality[1], "=")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
f, err := strconv.ParseFloat(parts[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
traceLogger.Printf("unable to parse quality in %s, %v", each, err)
|
||||||
|
} else {
|
||||||
|
sorted = insertMime(sorted, mime{typeAndQuality[0], f})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method
|
||||||
|
// and provides the response with a set of allowed methods for the request URL Path.
|
||||||
|
// As for any filter, you can also install it for a particular WebService within a Container.
|
||||||
|
// Note: this filter is not needed when using CrossOriginResourceSharing (for CORS).
|
||||||
|
func (c *Container) OPTIONSFilter(req *Request, resp *Response, chain *FilterChain) {
|
||||||
|
if "OPTIONS" != req.Request.Method {
|
||||||
|
chain.ProcessFilter(req, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
archs := req.Request.Header.Get(HEADER_AccessControlRequestHeaders)
|
||||||
|
methods := strings.Join(c.computeAllowedMethods(req), ",")
|
||||||
|
origin := req.Request.Header.Get(HEADER_Origin)
|
||||||
|
|
||||||
|
resp.AddHeader(HEADER_Allow, methods)
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowOrigin, origin)
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowHeaders, archs)
|
||||||
|
resp.AddHeader(HEADER_AccessControlAllowMethods, methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIONSFilter is a filter function that inspects the Http Request for the OPTIONS method
|
||||||
|
// and provides the response with a set of allowed methods for the request URL Path.
|
||||||
|
// Note: this filter is not needed when using CrossOriginResourceSharing (for CORS).
|
||||||
|
func OPTIONSFilter() FilterFunction {
|
||||||
|
return DefaultContainer.OPTIONSFilter
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PathParameterKind = indicator of Request parameter type "path"
|
||||||
|
PathParameterKind = iota
|
||||||
|
|
||||||
|
// QueryParameterKind = indicator of Request parameter type "query"
|
||||||
|
QueryParameterKind
|
||||||
|
|
||||||
|
// BodyParameterKind = indicator of Request parameter type "body"
|
||||||
|
BodyParameterKind
|
||||||
|
|
||||||
|
// HeaderParameterKind = indicator of Request parameter type "header"
|
||||||
|
HeaderParameterKind
|
||||||
|
|
||||||
|
// FormParameterKind = indicator of Request parameter type "form"
|
||||||
|
FormParameterKind
|
||||||
|
|
||||||
|
// CollectionFormatCSV comma separated values `foo,bar`
|
||||||
|
CollectionFormatCSV = CollectionFormat("csv")
|
||||||
|
|
||||||
|
// CollectionFormatSSV space separated values `foo bar`
|
||||||
|
CollectionFormatSSV = CollectionFormat("ssv")
|
||||||
|
|
||||||
|
// CollectionFormatTSV tab separated values `foo\tbar`
|
||||||
|
CollectionFormatTSV = CollectionFormat("tsv")
|
||||||
|
|
||||||
|
// CollectionFormatPipes pipe separated values `foo|bar`
|
||||||
|
CollectionFormatPipes = CollectionFormat("pipes")
|
||||||
|
|
||||||
|
// CollectionFormatMulti corresponds to multiple parameter instances instead of multiple values for a single
|
||||||
|
// instance `foo=bar&foo=baz`. This is valid only for QueryParameters and FormParameters
|
||||||
|
CollectionFormatMulti = CollectionFormat("multi")
|
||||||
|
)
|
||||||
|
|
||||||
|
type CollectionFormat string
|
||||||
|
|
||||||
|
func (cf CollectionFormat) String() string {
|
||||||
|
return string(cf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter is for documententing the parameter used in a Http Request
|
||||||
|
// ParameterData kinds are Path,Query and Body
|
||||||
|
type Parameter struct {
|
||||||
|
data *ParameterData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterData represents the state of a Parameter.
|
||||||
|
// It is made public to make it accessible to e.g. the Swagger package.
|
||||||
|
type ParameterData struct {
|
||||||
|
Name, Description, DataType, DataFormat string
|
||||||
|
Kind int
|
||||||
|
Required bool
|
||||||
|
AllowableValues map[string]string
|
||||||
|
AllowMultiple bool
|
||||||
|
DefaultValue string
|
||||||
|
CollectionFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the state of the Parameter
|
||||||
|
func (p *Parameter) Data() ParameterData {
|
||||||
|
return *p.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns the parameter type indicator (see const for valid values)
|
||||||
|
func (p *Parameter) Kind() int {
|
||||||
|
return p.data.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) bePath() *Parameter {
|
||||||
|
p.data.Kind = PathParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Parameter) beQuery() *Parameter {
|
||||||
|
p.data.Kind = QueryParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Parameter) beBody() *Parameter {
|
||||||
|
p.data.Kind = BodyParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) beHeader() *Parameter {
|
||||||
|
p.data.Kind = HeaderParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parameter) beForm() *Parameter {
|
||||||
|
p.data.Kind = FormParameterKind
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required sets the required field and returns the receiver
|
||||||
|
func (p *Parameter) Required(required bool) *Parameter {
|
||||||
|
p.data.Required = required
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowMultiple sets the allowMultiple field and returns the receiver
|
||||||
|
func (p *Parameter) AllowMultiple(multiple bool) *Parameter {
|
||||||
|
p.data.AllowMultiple = multiple
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowableValues sets the allowableValues field and returns the receiver
|
||||||
|
func (p *Parameter) AllowableValues(values map[string]string) *Parameter {
|
||||||
|
p.data.AllowableValues = values
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataType sets the dataType field and returns the receiver
|
||||||
|
func (p *Parameter) DataType(typeName string) *Parameter {
|
||||||
|
p.data.DataType = typeName
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataFormat sets the dataFormat field for Swagger UI
|
||||||
|
func (p *Parameter) DataFormat(formatName string) *Parameter {
|
||||||
|
p.data.DataFormat = formatName
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultValue sets the default value field and returns the receiver
|
||||||
|
func (p *Parameter) DefaultValue(stringRepresentation string) *Parameter {
|
||||||
|
p.data.DefaultValue = stringRepresentation
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description sets the description value field and returns the receiver
|
||||||
|
func (p *Parameter) Description(doc string) *Parameter {
|
||||||
|
p.data.Description = doc
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionFormat sets the collection format for an array type
|
||||||
|
func (p *Parameter) CollectionFormat(format CollectionFormat) *Parameter {
|
||||||
|
p.data.CollectionFormat = format.String()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PathExpression holds a compiled path expression (RegExp) needed to match against
|
||||||
|
// Http request paths and to extract path parameter values.
|
||||||
|
type pathExpression struct {
|
||||||
|
LiteralCount int // the number of literal characters (means those not resulting from template variable substitution)
|
||||||
|
VarNames []string // the names of parameters (enclosed by {}) in the path
|
||||||
|
VarCount int // the number of named parameters (enclosed by {}) in the path
|
||||||
|
Matcher *regexp.Regexp
|
||||||
|
Source string // Path as defined by the RouteBuilder
|
||||||
|
tokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPathExpression creates a PathExpression from the input URL path.
|
||||||
|
// Returns an error if the path is invalid.
|
||||||
|
func newPathExpression(path string) (*pathExpression, error) {
|
||||||
|
expression, literalCount, varNames, varCount, tokens := templateToRegularExpression(path)
|
||||||
|
compiled, err := regexp.Compile(expression)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pathExpression{literalCount, varNames, varCount, compiled, expression, tokens}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://jsr311.java.net/nonav/releases/1.1/spec/spec3.html#x3-370003.7.3
|
||||||
|
func templateToRegularExpression(template string) (expression string, literalCount int, varNames []string, varCount int, tokens []string) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteString("^")
|
||||||
|
//tokens = strings.Split(template, "/")
|
||||||
|
tokens = tokenizePath(template)
|
||||||
|
for _, each := range tokens {
|
||||||
|
if each == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buffer.WriteString("/")
|
||||||
|
if strings.HasPrefix(each, "{") {
|
||||||
|
// check for regular expression in variable
|
||||||
|
colon := strings.Index(each, ":")
|
||||||
|
var varName string
|
||||||
|
if colon != -1 {
|
||||||
|
// extract expression
|
||||||
|
varName = strings.TrimSpace(each[1:colon])
|
||||||
|
paramExpr := strings.TrimSpace(each[colon+1 : len(each)-1])
|
||||||
|
if paramExpr == "*" { // special case
|
||||||
|
buffer.WriteString("(.*)")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(fmt.Sprintf("(%s)", paramExpr)) // between colon and closing moustache
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// plain var
|
||||||
|
varName = strings.TrimSpace(each[1 : len(each)-1])
|
||||||
|
buffer.WriteString("([^/]+?)")
|
||||||
|
}
|
||||||
|
varNames = append(varNames, varName)
|
||||||
|
varCount += 1
|
||||||
|
} else {
|
||||||
|
literalCount += len(each)
|
||||||
|
encoded := each // TODO URI encode
|
||||||
|
buffer.WriteString(regexp.QuoteMeta(encoded))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimRight(buffer.String(), "/") + "(/.*)?$", literalCount, varNames, varCount, tokens
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copyright 2018 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// PathProcessor is extra behaviour that a Router can provide to extract path parameters from the path.
|
||||||
|
// If a Router does not implement this interface then the default behaviour will be used.
|
||||||
|
type PathProcessor interface {
|
||||||
|
// ExtractParameters gets the path parameters defined in the route and webService from the urlPath
|
||||||
|
ExtractParameters(route *Route, webService *WebService, urlPath string) map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultPathProcessor struct{}
|
||||||
|
|
||||||
|
// Extract the parameters from the request url path
|
||||||
|
func (d defaultPathProcessor) ExtractParameters(r *Route, _ *WebService, urlPath string) map[string]string {
|
||||||
|
urlParts := tokenizePath(urlPath)
|
||||||
|
pathParameters := map[string]string{}
|
||||||
|
for i, key := range r.pathParts {
|
||||||
|
var value string
|
||||||
|
if i >= len(urlParts) {
|
||||||
|
value = ""
|
||||||
|
} else {
|
||||||
|
value = urlParts[i]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, "{") { // path-parameter
|
||||||
|
if colon := strings.Index(key, ":"); colon != -1 {
|
||||||
|
// extract by regex
|
||||||
|
regPart := key[colon+1 : len(key)-1]
|
||||||
|
keyPart := key[1:colon]
|
||||||
|
if regPart == "*" {
|
||||||
|
pathParameters[keyPart] = untokenizePath(i, urlParts)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
pathParameters[keyPart] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// without enclosing {}
|
||||||
|
pathParameters[key[1:len(key)-1]] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untokenize back into an URL path using the slash separator
|
||||||
|
func untokenizePath(offset int, parts []string) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for p := offset; p < len(parts); p++ {
|
||||||
|
buffer.WriteString(parts[p])
|
||||||
|
// do not end
|
||||||
|
if p < len(parts)-1 {
|
||||||
|
buffer.WriteString("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/zlib"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultRequestContentType string
|
||||||
|
|
||||||
|
// Request is a wrapper for a http Request that provides convenience methods
|
||||||
|
type Request struct {
|
||||||
|
Request *http.Request
|
||||||
|
pathParameters map[string]string
|
||||||
|
attributes map[string]interface{} // for storing request-scoped values
|
||||||
|
selectedRoutePath string // root path + route path that matched the request, e.g. /meetings/{id}/attendees
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest(httpRequest *http.Request) *Request {
|
||||||
|
return &Request{
|
||||||
|
Request: httpRequest,
|
||||||
|
pathParameters: map[string]string{},
|
||||||
|
attributes: map[string]interface{}{},
|
||||||
|
} // empty parameters, attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ContentType is missing or */* is given then fall back to this type, otherwise
|
||||||
|
// a "Unable to unmarshal content of type:" response is returned.
|
||||||
|
// Valid values are restful.MIME_JSON and restful.MIME_XML
|
||||||
|
// Example:
|
||||||
|
// restful.DefaultRequestContentType(restful.MIME_JSON)
|
||||||
|
func DefaultRequestContentType(mime string) {
|
||||||
|
defaultRequestContentType = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameter accesses the Path parameter value by its name
|
||||||
|
func (r *Request) PathParameter(name string) string {
|
||||||
|
return r.pathParameters[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameters accesses the Path parameter values
|
||||||
|
func (r *Request) PathParameters() map[string]string {
|
||||||
|
return r.pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameter returns the (first) Query parameter value by its name
|
||||||
|
func (r *Request) QueryParameter(name string) string {
|
||||||
|
return r.Request.FormValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameters returns the all the query parameters values by name
|
||||||
|
func (r *Request) QueryParameters(name string) []string {
|
||||||
|
return r.Request.URL.Query()[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyParameter parses the body of the request (once for typically a POST or a PUT) and returns the value of the given name or an error.
|
||||||
|
func (r *Request) BodyParameter(name string) (string, error) {
|
||||||
|
err := r.Request.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.Request.PostFormValue(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderParameter returns the HTTP Header value of a Header name or empty if missing
|
||||||
|
func (r *Request) HeaderParameter(name string) string {
|
||||||
|
return r.Request.Header.Get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEntity checks the Accept header and reads the content into the entityPointer.
|
||||||
|
func (r *Request) ReadEntity(entityPointer interface{}) (err error) {
|
||||||
|
contentType := r.Request.Header.Get(HEADER_ContentType)
|
||||||
|
contentEncoding := r.Request.Header.Get(HEADER_ContentEncoding)
|
||||||
|
|
||||||
|
// check if the request body needs decompression
|
||||||
|
if ENCODING_GZIP == contentEncoding {
|
||||||
|
gzipReader := currentCompressorProvider.AcquireGzipReader()
|
||||||
|
defer currentCompressorProvider.ReleaseGzipReader(gzipReader)
|
||||||
|
gzipReader.Reset(r.Request.Body)
|
||||||
|
r.Request.Body = gzipReader
|
||||||
|
} else if ENCODING_DEFLATE == contentEncoding {
|
||||||
|
zlibReader, err := zlib.NewReader(r.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Request.Body = zlibReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup the EntityReader, use defaultRequestContentType if needed and provided
|
||||||
|
entityReader, ok := entityAccessRegistry.accessorAt(contentType)
|
||||||
|
if !ok {
|
||||||
|
if len(defaultRequestContentType) != 0 {
|
||||||
|
entityReader, ok = entityAccessRegistry.accessorAt(defaultRequestContentType)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return NewError(http.StatusBadRequest, "Unable to unmarshal content of type:"+contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entityReader.Read(r, entityPointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAttribute adds or replaces the attribute with the given value.
|
||||||
|
func (r *Request) SetAttribute(name string, value interface{}) {
|
||||||
|
r.attributes[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute returns the value associated to the given name. Returns nil if absent.
|
||||||
|
func (r Request) Attribute(name string) interface{} {
|
||||||
|
return r.attributes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedRoutePath root path + route path that matched the request, e.g. /meetings/{id}/attendees
|
||||||
|
func (r Request) SelectedRoutePath() string {
|
||||||
|
return r.selectedRoutePath
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultResponseMimeType is DEPRECATED, use DefaultResponseContentType(mime)
|
||||||
|
var DefaultResponseMimeType string
|
||||||
|
|
||||||
|
//PrettyPrintResponses controls the indentation feature of XML and JSON serialization
|
||||||
|
var PrettyPrintResponses = true
|
||||||
|
|
||||||
|
// Response is a wrapper on the actual http ResponseWriter
|
||||||
|
// It provides several convenience methods to prepare and write response content.
|
||||||
|
type Response struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
requestAccept string // mime-type what the Http Request says it wants to receive
|
||||||
|
routeProduces []string // mime-types what the Route says it can produce
|
||||||
|
statusCode int // HTTP status code that has been written explicitly (if zero then net/http has written 200)
|
||||||
|
contentLength int // number of bytes written for the response body
|
||||||
|
prettyPrint bool // controls the indentation feature of XML and JSON serialization. It is initialized using var PrettyPrintResponses.
|
||||||
|
err error // err property is kept when WriteError is called
|
||||||
|
hijacker http.Hijacker // if underlying ResponseWriter supports it
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResponse creates a new response based on a http ResponseWriter.
|
||||||
|
func NewResponse(httpWriter http.ResponseWriter) *Response {
|
||||||
|
hijacker, _ := httpWriter.(http.Hijacker)
|
||||||
|
return &Response{ResponseWriter: httpWriter, routeProduces: []string{}, statusCode: http.StatusOK, prettyPrint: PrettyPrintResponses, hijacker: hijacker}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultResponseContentType set a default.
|
||||||
|
// If Accept header matching fails, fall back to this type.
|
||||||
|
// Valid values are restful.MIME_JSON and restful.MIME_XML
|
||||||
|
// Example:
|
||||||
|
// restful.DefaultResponseContentType(restful.MIME_JSON)
|
||||||
|
func DefaultResponseContentType(mime string) {
|
||||||
|
DefaultResponseMimeType = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalServerError writes the StatusInternalServerError header.
|
||||||
|
// DEPRECATED, use WriteErrorString(http.StatusInternalServerError,reason)
|
||||||
|
func (r Response) InternalServerError() Response {
|
||||||
|
r.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack implements the http.Hijacker interface. This expands
|
||||||
|
// the Response to fulfill http.Hijacker if the underlying
|
||||||
|
// http.ResponseWriter supports it.
|
||||||
|
func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
if r.hijacker == nil {
|
||||||
|
return nil, nil, errors.New("http.Hijacker not implemented by underlying http.ResponseWriter")
|
||||||
|
}
|
||||||
|
return r.hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrettyPrint changes whether this response must produce pretty (line-by-line, indented) JSON or XML output.
|
||||||
|
func (r *Response) PrettyPrint(bePretty bool) {
|
||||||
|
r.prettyPrint = bePretty
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHeader is a shortcut for .Header().Add(header,value)
|
||||||
|
func (r Response) AddHeader(header string, value string) Response {
|
||||||
|
r.Header().Add(header, value)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequestAccepts tells the response what Mime-type(s) the HTTP request said it wants to accept. Exposed for testing.
|
||||||
|
func (r *Response) SetRequestAccepts(mime string) {
|
||||||
|
r.requestAccept = mime
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntityWriter returns the registered EntityWriter that the entity (requested resource)
|
||||||
|
// can write according to what the request wants (Accept) and what the Route can produce or what the restful defaults say.
|
||||||
|
// If called before WriteEntity and WriteHeader then a false return value can be used to write a 406: Not Acceptable.
|
||||||
|
func (r *Response) EntityWriter() (EntityReaderWriter, bool) {
|
||||||
|
sorted := sortedMimes(r.requestAccept)
|
||||||
|
for _, eachAccept := range sorted {
|
||||||
|
for _, eachProduce := range r.routeProduces {
|
||||||
|
if eachProduce == eachAccept.media {
|
||||||
|
if w, ok := entityAccessRegistry.accessorAt(eachAccept.media); ok {
|
||||||
|
return w, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if eachAccept.media == "*/*" {
|
||||||
|
for _, each := range r.routeProduces {
|
||||||
|
if w, ok := entityAccessRegistry.accessorAt(each); ok {
|
||||||
|
return w, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if requestAccept is empty
|
||||||
|
writer, ok := entityAccessRegistry.accessorAt(r.requestAccept)
|
||||||
|
if !ok {
|
||||||
|
// if not registered then fallback to the defaults (if set)
|
||||||
|
if DefaultResponseMimeType == MIME_JSON {
|
||||||
|
return entityAccessRegistry.accessorAt(MIME_JSON)
|
||||||
|
}
|
||||||
|
if DefaultResponseMimeType == MIME_XML {
|
||||||
|
return entityAccessRegistry.accessorAt(MIME_XML)
|
||||||
|
}
|
||||||
|
// Fallback to whatever the route says it can produce.
|
||||||
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||||
|
for _, each := range r.routeProduces {
|
||||||
|
if w, ok := entityAccessRegistry.accessorAt(each); ok {
|
||||||
|
return w, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if trace {
|
||||||
|
traceLogger.Printf("no registered EntityReaderWriter found for %s", r.requestAccept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEntity calls WriteHeaderAndEntity with Http Status OK (200)
|
||||||
|
func (r *Response) WriteEntity(value interface{}) error {
|
||||||
|
return r.WriteHeaderAndEntity(http.StatusOK, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeaderAndEntity marshals the value using the representation denoted by the Accept Header and the registered EntityWriters.
|
||||||
|
// If no Accept header is specified (or */*) then respond with the Content-Type as specified by the first in the Route.Produces.
|
||||||
|
// If an Accept header is specified then respond with the Content-Type as specified by the first in the Route.Produces that is matched with the Accept header.
|
||||||
|
// If the value is nil then no response is send except for the Http status. You may want to call WriteHeader(http.StatusNotFound) instead.
|
||||||
|
// If there is no writer available that can represent the value in the requested MIME type then Http Status NotAcceptable is written.
|
||||||
|
// Current implementation ignores any q-parameters in the Accept Header.
|
||||||
|
// Returns an error if the value could not be written on the response.
|
||||||
|
func (r *Response) WriteHeaderAndEntity(status int, value interface{}) error {
|
||||||
|
writer, ok := r.EntityWriter()
|
||||||
|
if !ok {
|
||||||
|
r.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return writer.Write(r, status, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAsXml is a convenience method for writing a value in xml (requires Xml tags on the value)
|
||||||
|
// It uses the standard encoding/xml package for marshalling the value ; not using a registered EntityReaderWriter.
|
||||||
|
func (r *Response) WriteAsXml(value interface{}) error {
|
||||||
|
return writeXML(r, http.StatusOK, MIME_XML, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeaderAndXml is a convenience method for writing a status and value in xml (requires Xml tags on the value)
|
||||||
|
// It uses the standard encoding/xml package for marshalling the value ; not using a registered EntityReaderWriter.
|
||||||
|
func (r *Response) WriteHeaderAndXml(status int, value interface{}) error {
|
||||||
|
return writeXML(r, status, MIME_XML, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAsJson is a convenience method for writing a value in json.
|
||||||
|
// It uses the standard encoding/json package for marshalling the value ; not using a registered EntityReaderWriter.
|
||||||
|
func (r *Response) WriteAsJson(value interface{}) error {
|
||||||
|
return writeJSON(r, http.StatusOK, MIME_JSON, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJson is a convenience method for writing a value in Json with a given Content-Type.
|
||||||
|
// It uses the standard encoding/json package for marshalling the value ; not using a registered EntityReaderWriter.
|
||||||
|
func (r *Response) WriteJson(value interface{}, contentType string) error {
|
||||||
|
return writeJSON(r, http.StatusOK, contentType, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeaderAndJson is a convenience method for writing the status and a value in Json with a given Content-Type.
|
||||||
|
// It uses the standard encoding/json package for marshalling the value ; not using a registered EntityReaderWriter.
|
||||||
|
func (r *Response) WriteHeaderAndJson(status int, value interface{}, contentType string) error {
|
||||||
|
return writeJSON(r, status, contentType, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteError write the http status and the error string on the response.
|
||||||
|
func (r *Response) WriteError(httpStatus int, err error) error {
|
||||||
|
r.err = err
|
||||||
|
return r.WriteErrorString(httpStatus, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteServiceError is a convenience method for a responding with a status and a ServiceError
|
||||||
|
func (r *Response) WriteServiceError(httpStatus int, err ServiceError) error {
|
||||||
|
r.err = err
|
||||||
|
return r.WriteHeaderAndEntity(httpStatus, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteErrorString is a convenience method for an error status with the actual error
|
||||||
|
func (r *Response) WriteErrorString(httpStatus int, errorReason string) error {
|
||||||
|
if r.err == nil {
|
||||||
|
// if not called from WriteError
|
||||||
|
r.err = errors.New(errorReason)
|
||||||
|
}
|
||||||
|
r.WriteHeader(httpStatus)
|
||||||
|
if _, err := r.Write([]byte(errorReason)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush implements http.Flusher interface, which sends any buffered data to the client.
|
||||||
|
func (r *Response) Flush() {
|
||||||
|
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
} else if trace {
|
||||||
|
traceLogger.Printf("ResponseWriter %v doesn't support Flush", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader is overridden to remember the Status Code that has been written.
|
||||||
|
// Changes to the Header of the response have no effect after this.
|
||||||
|
func (r *Response) WriteHeader(httpStatus int) {
|
||||||
|
r.statusCode = httpStatus
|
||||||
|
r.ResponseWriter.WriteHeader(httpStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode returns the code that has been written using WriteHeader.
|
||||||
|
func (r Response) StatusCode() int {
|
||||||
|
if 0 == r.statusCode {
|
||||||
|
// no status code has been written yet; assume OK
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
return r.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the data to the connection as part of an HTTP reply.
|
||||||
|
// Write is part of http.ResponseWriter interface.
|
||||||
|
func (r *Response) Write(bytes []byte) (int, error) {
|
||||||
|
written, err := r.ResponseWriter.Write(bytes)
|
||||||
|
r.contentLength += written
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentLength returns the number of bytes written for the response content.
|
||||||
|
// Note that this value is only correct if all data is written through the Response using its Write* methods.
|
||||||
|
// Data written directly using the underlying http.ResponseWriter is not accounted for.
|
||||||
|
func (r Response) ContentLength() int {
|
||||||
|
return r.contentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseNotify is part of http.CloseNotifier interface
|
||||||
|
func (r Response) CloseNotify() <-chan bool {
|
||||||
|
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the err created by WriteError
|
||||||
|
func (r Response) Error() error {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteFunction declares the signature of a function that can be bound to a Route.
|
||||||
|
type RouteFunction func(*Request, *Response)
|
||||||
|
|
||||||
|
// RouteSelectionConditionFunction declares the signature of a function that
|
||||||
|
// can be used to add extra conditional logic when selecting whether the route
|
||||||
|
// matches the HTTP request.
|
||||||
|
type RouteSelectionConditionFunction func(httpRequest *http.Request) bool
|
||||||
|
|
||||||
|
// Route binds a HTTP Method,Path,Consumes combination to a RouteFunction.
|
||||||
|
type Route struct {
|
||||||
|
Method string
|
||||||
|
Produces []string
|
||||||
|
Consumes []string
|
||||||
|
Path string // webservice root path + described path
|
||||||
|
Function RouteFunction
|
||||||
|
Filters []FilterFunction
|
||||||
|
If []RouteSelectionConditionFunction
|
||||||
|
|
||||||
|
// cached values for dispatching
|
||||||
|
relativePath string
|
||||||
|
pathParts []string
|
||||||
|
pathExpr *pathExpression // cached compilation of relativePath as RegExp
|
||||||
|
|
||||||
|
// documentation
|
||||||
|
Doc string
|
||||||
|
Notes string
|
||||||
|
Operation string
|
||||||
|
ParameterDocs []*Parameter
|
||||||
|
ResponseErrors map[int]ResponseError
|
||||||
|
ReadSample, WriteSample interface{} // structs that model an example request or response payload
|
||||||
|
|
||||||
|
// Extra information used to store custom information about the route.
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
|
||||||
|
// marks a route as deprecated
|
||||||
|
Deprecated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize for Route
|
||||||
|
func (r *Route) postBuild() {
|
||||||
|
r.pathParts = tokenizePath(r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Request and Response from their http versions
|
||||||
|
func (r *Route) wrapRequestResponse(httpWriter http.ResponseWriter, httpRequest *http.Request, pathParams map[string]string) (*Request, *Response) {
|
||||||
|
wrappedRequest := NewRequest(httpRequest)
|
||||||
|
wrappedRequest.pathParameters = pathParams
|
||||||
|
wrappedRequest.selectedRoutePath = r.Path
|
||||||
|
wrappedResponse := NewResponse(httpWriter)
|
||||||
|
wrappedResponse.requestAccept = httpRequest.Header.Get(HEADER_Accept)
|
||||||
|
wrappedResponse.routeProduces = r.Produces
|
||||||
|
return wrappedRequest, wrappedResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchWithFilters call the function after passing through its own filters
|
||||||
|
func (r *Route) dispatchWithFilters(wrappedRequest *Request, wrappedResponse *Response) {
|
||||||
|
if len(r.Filters) > 0 {
|
||||||
|
chain := FilterChain{Filters: r.Filters, Target: r.Function}
|
||||||
|
chain.ProcessFilter(wrappedRequest, wrappedResponse)
|
||||||
|
} else {
|
||||||
|
// unfiltered
|
||||||
|
r.Function(wrappedRequest, wrappedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whether the mimeType matches to what this Route can produce.
|
||||||
|
func (r Route) matchesAccept(mimeTypesWithQuality string) bool {
|
||||||
|
parts := strings.Split(mimeTypesWithQuality, ",")
|
||||||
|
for _, each := range parts {
|
||||||
|
var withoutQuality string
|
||||||
|
if strings.Contains(each, ";") {
|
||||||
|
withoutQuality = strings.Split(each, ";")[0]
|
||||||
|
} else {
|
||||||
|
withoutQuality = each
|
||||||
|
}
|
||||||
|
// trim before compare
|
||||||
|
withoutQuality = strings.Trim(withoutQuality, " ")
|
||||||
|
if withoutQuality == "*/*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, producibleType := range r.Produces {
|
||||||
|
if producibleType == "*/*" || producibleType == withoutQuality {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return whether this Route can consume content with a type specified by mimeTypes (can be empty).
|
||||||
|
func (r Route) matchesContentType(mimeTypes string) bool {
|
||||||
|
|
||||||
|
if len(r.Consumes) == 0 {
|
||||||
|
// did not specify what it can consume ; any media type (“*/*”) is assumed
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mimeTypes) == 0 {
|
||||||
|
// idempotent methods with (most-likely or guaranteed) empty content match missing Content-Type
|
||||||
|
m := r.Method
|
||||||
|
if m == "GET" || m == "HEAD" || m == "OPTIONS" || m == "DELETE" || m == "TRACE" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// proceed with default
|
||||||
|
mimeTypes = MIME_OCTET
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(mimeTypes, ",")
|
||||||
|
for _, each := range parts {
|
||||||
|
var contentType string
|
||||||
|
if strings.Contains(each, ";") {
|
||||||
|
contentType = strings.Split(each, ";")[0]
|
||||||
|
} else {
|
||||||
|
contentType = each
|
||||||
|
}
|
||||||
|
// trim before compare
|
||||||
|
contentType = strings.Trim(contentType, " ")
|
||||||
|
for _, consumeableType := range r.Consumes {
|
||||||
|
if consumeableType == "*/*" || consumeableType == contentType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize an URL path using the slash separator ; the result does not have empty tokens
|
||||||
|
func tokenizePath(path string) []string {
|
||||||
|
if "/" == path {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// for debugging
|
||||||
|
func (r Route) String() string {
|
||||||
|
return r.Method + " " + r.Path
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteBuilder is a helper to construct Routes.
|
||||||
|
type RouteBuilder struct {
|
||||||
|
rootPath string
|
||||||
|
currentPath string
|
||||||
|
produces []string
|
||||||
|
consumes []string
|
||||||
|
httpMethod string // required
|
||||||
|
function RouteFunction // required
|
||||||
|
filters []FilterFunction
|
||||||
|
conditions []RouteSelectionConditionFunction
|
||||||
|
|
||||||
|
typeNameHandleFunc TypeNameHandleFunction // required
|
||||||
|
|
||||||
|
// documentation
|
||||||
|
doc string
|
||||||
|
notes string
|
||||||
|
operation string
|
||||||
|
readSample, writeSample interface{}
|
||||||
|
parameters []*Parameter
|
||||||
|
errorMap map[int]ResponseError
|
||||||
|
metadata map[string]interface{}
|
||||||
|
deprecated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do evaluates each argument with the RouteBuilder itself.
|
||||||
|
// This allows you to follow DRY principles without breaking the fluent programming style.
|
||||||
|
// Example:
|
||||||
|
// ws.Route(ws.DELETE("/{name}").To(t.deletePerson).Do(Returns200, Returns500))
|
||||||
|
//
|
||||||
|
// func Returns500(b *RouteBuilder) {
|
||||||
|
// b.Returns(500, "Internal Server Error", restful.ServiceError{})
|
||||||
|
// }
|
||||||
|
func (b *RouteBuilder) Do(oneArgBlocks ...func(*RouteBuilder)) *RouteBuilder {
|
||||||
|
for _, each := range oneArgBlocks {
|
||||||
|
each(b)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// To bind the route to a function.
|
||||||
|
// If this route is matched with the incoming Http Request then call this function with the *Request,*Response pair. Required.
|
||||||
|
func (b *RouteBuilder) To(function RouteFunction) *RouteBuilder {
|
||||||
|
b.function = function
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method specifies what HTTP method to match. Required.
|
||||||
|
func (b *RouteBuilder) Method(method string) *RouteBuilder {
|
||||||
|
b.httpMethod = method
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces specifies what MIME types can be produced ; the matched one will appear in the Content-Type Http header.
|
||||||
|
func (b *RouteBuilder) Produces(mimeTypes ...string) *RouteBuilder {
|
||||||
|
b.produces = mimeTypes
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumes specifies what MIME types can be consumes ; the Accept Http header must matched any of these
|
||||||
|
func (b *RouteBuilder) Consumes(mimeTypes ...string) *RouteBuilder {
|
||||||
|
b.consumes = mimeTypes
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path specifies the relative (w.r.t WebService root path) URL path to match. Default is "/".
|
||||||
|
func (b *RouteBuilder) Path(subPath string) *RouteBuilder {
|
||||||
|
b.currentPath = subPath
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doc tells what this route is all about. Optional.
|
||||||
|
func (b *RouteBuilder) Doc(documentation string) *RouteBuilder {
|
||||||
|
b.doc = documentation
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes is a verbose explanation of the operation behavior. Optional.
|
||||||
|
func (b *RouteBuilder) Notes(notes string) *RouteBuilder {
|
||||||
|
b.notes = notes
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads tells what resource type will be read from the request payload. Optional.
|
||||||
|
// A parameter of type "body" is added ,required is set to true and the dataType is set to the qualified name of the sample's type.
|
||||||
|
func (b *RouteBuilder) Reads(sample interface{}, optionalDescription ...string) *RouteBuilder {
|
||||||
|
fn := b.typeNameHandleFunc
|
||||||
|
if fn == nil {
|
||||||
|
fn = reflectTypeName
|
||||||
|
}
|
||||||
|
typeAsName := fn(sample)
|
||||||
|
description := ""
|
||||||
|
if len(optionalDescription) > 0 {
|
||||||
|
description = optionalDescription[0]
|
||||||
|
}
|
||||||
|
b.readSample = sample
|
||||||
|
bodyParameter := &Parameter{&ParameterData{Name: "body", Description: description}}
|
||||||
|
bodyParameter.beBody()
|
||||||
|
bodyParameter.Required(true)
|
||||||
|
bodyParameter.DataType(typeAsName)
|
||||||
|
b.Param(bodyParameter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterNamed returns a Parameter already known to the RouteBuilder. Returns nil if not.
|
||||||
|
// Use this to modify or extend information for the Parameter (through its Data()).
|
||||||
|
func (b RouteBuilder) ParameterNamed(name string) (p *Parameter) {
|
||||||
|
for _, each := range b.parameters {
|
||||||
|
if each.Data().Name == name {
|
||||||
|
return each
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes tells what resource type will be written as the response payload. Optional.
|
||||||
|
func (b *RouteBuilder) Writes(sample interface{}) *RouteBuilder {
|
||||||
|
b.writeSample = sample
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param allows you to document the parameters of the Route. It adds a new Parameter (does not check for duplicates).
|
||||||
|
func (b *RouteBuilder) Param(parameter *Parameter) *RouteBuilder {
|
||||||
|
if b.parameters == nil {
|
||||||
|
b.parameters = []*Parameter{}
|
||||||
|
}
|
||||||
|
b.parameters = append(b.parameters, parameter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation allows you to document what the actual method/function call is of the Route.
|
||||||
|
// Unless called, the operation name is derived from the RouteFunction set using To(..).
|
||||||
|
func (b *RouteBuilder) Operation(name string) *RouteBuilder {
|
||||||
|
b.operation = name
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnsError is deprecated, use Returns instead.
|
||||||
|
func (b *RouteBuilder) ReturnsError(code int, message string, model interface{}) *RouteBuilder {
|
||||||
|
log.Print("ReturnsError is deprecated, use Returns instead.")
|
||||||
|
return b.Returns(code, message, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns allows you to document what responses (errors or regular) can be expected.
|
||||||
|
// The model parameter is optional ; either pass a struct instance or use nil if not applicable.
|
||||||
|
func (b *RouteBuilder) Returns(code int, message string, model interface{}) *RouteBuilder {
|
||||||
|
err := ResponseError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Model: model,
|
||||||
|
IsDefault: false,
|
||||||
|
}
|
||||||
|
// lazy init because there is no NewRouteBuilder (yet)
|
||||||
|
if b.errorMap == nil {
|
||||||
|
b.errorMap = map[int]ResponseError{}
|
||||||
|
}
|
||||||
|
b.errorMap[code] = err
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultReturns is a special Returns call that sets the default of the response ; the code is zero.
|
||||||
|
func (b *RouteBuilder) DefaultReturns(message string, model interface{}) *RouteBuilder {
|
||||||
|
b.Returns(0, message, model)
|
||||||
|
// Modify the ResponseError just added/updated
|
||||||
|
re := b.errorMap[0]
|
||||||
|
// errorMap is initialized
|
||||||
|
b.errorMap[0] = ResponseError{
|
||||||
|
Code: re.Code,
|
||||||
|
Message: re.Message,
|
||||||
|
Model: re.Model,
|
||||||
|
IsDefault: true,
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata adds or updates a key=value pair to the metadata map.
|
||||||
|
func (b *RouteBuilder) Metadata(key string, value interface{}) *RouteBuilder {
|
||||||
|
if b.metadata == nil {
|
||||||
|
b.metadata = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
b.metadata[key] = value
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecate sets the value of deprecated to true. Deprecated routes have a special UI treatment to warn against use
|
||||||
|
func (b *RouteBuilder) Deprecate() *RouteBuilder {
|
||||||
|
b.deprecated = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseError represents a response; not necessarily an error.
|
||||||
|
type ResponseError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
Model interface{}
|
||||||
|
IsDefault bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RouteBuilder) servicePath(path string) *RouteBuilder {
|
||||||
|
b.rootPath = path
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends a FilterFunction to the end of filters for this Route to build.
|
||||||
|
func (b *RouteBuilder) Filter(filter FilterFunction) *RouteBuilder {
|
||||||
|
b.filters = append(b.filters, filter)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sets a condition function that controls matching the Route based on custom logic.
|
||||||
|
// The condition function is provided the HTTP request and should return true if the route
|
||||||
|
// should be considered.
|
||||||
|
//
|
||||||
|
// Efficiency note: the condition function is called before checking the method, produces, and
|
||||||
|
// consumes criteria, so that the correct HTTP status code can be returned.
|
||||||
|
//
|
||||||
|
// Lifecycle note: no filter functions have been called prior to calling the condition function,
|
||||||
|
// so the condition function should not depend on any context that might be set up by container
|
||||||
|
// or route filters.
|
||||||
|
func (b *RouteBuilder) If(condition RouteSelectionConditionFunction) *RouteBuilder {
|
||||||
|
b.conditions = append(b.conditions, condition)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific Route path then set to rootPath
|
||||||
|
// If no specific Produces then set to rootProduces
|
||||||
|
// If no specific Consumes then set to rootConsumes
|
||||||
|
func (b *RouteBuilder) copyDefaults(rootProduces, rootConsumes []string) {
|
||||||
|
if len(b.produces) == 0 {
|
||||||
|
b.produces = rootProduces
|
||||||
|
}
|
||||||
|
if len(b.consumes) == 0 {
|
||||||
|
b.consumes = rootConsumes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeNameHandler sets the function that will convert types to strings in the parameter
|
||||||
|
// and model definitions.
|
||||||
|
func (b *RouteBuilder) typeNameHandler(handler TypeNameHandleFunction) *RouteBuilder {
|
||||||
|
b.typeNameHandleFunc = handler
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build creates a new Route using the specification details collected by the RouteBuilder
|
||||||
|
func (b *RouteBuilder) Build() Route {
|
||||||
|
pathExpr, err := newPathExpression(b.currentPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[restful] Invalid path:%s because:%v", b.currentPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if b.function == nil {
|
||||||
|
log.Printf("[restful] No function specified for route:" + b.currentPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
operationName := b.operation
|
||||||
|
if len(operationName) == 0 && b.function != nil {
|
||||||
|
// extract from definition
|
||||||
|
operationName = nameOfFunction(b.function)
|
||||||
|
}
|
||||||
|
route := Route{
|
||||||
|
Method: b.httpMethod,
|
||||||
|
Path: concatPath(b.rootPath, b.currentPath),
|
||||||
|
Produces: b.produces,
|
||||||
|
Consumes: b.consumes,
|
||||||
|
Function: b.function,
|
||||||
|
Filters: b.filters,
|
||||||
|
If: b.conditions,
|
||||||
|
relativePath: b.currentPath,
|
||||||
|
pathExpr: pathExpr,
|
||||||
|
Doc: b.doc,
|
||||||
|
Notes: b.notes,
|
||||||
|
Operation: operationName,
|
||||||
|
ParameterDocs: b.parameters,
|
||||||
|
ResponseErrors: b.errorMap,
|
||||||
|
ReadSample: b.readSample,
|
||||||
|
WriteSample: b.writeSample,
|
||||||
|
Metadata: b.metadata,
|
||||||
|
Deprecated: b.deprecated}
|
||||||
|
route.postBuild()
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
||||||
|
func concatPath(path1, path2 string) string {
|
||||||
|
return strings.TrimRight(path1, "/") + "/" + strings.TrimLeft(path2, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymousFuncCount int32
|
||||||
|
|
||||||
|
// nameOfFunction returns the short name of the function f for documentation.
|
||||||
|
// It uses a runtime feature for debugging ; its value may change for later Go versions.
|
||||||
|
func nameOfFunction(f interface{}) string {
|
||||||
|
fun := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
|
||||||
|
tokenized := strings.Split(fun.Name(), ".")
|
||||||
|
last := tokenized[len(tokenized)-1]
|
||||||
|
last = strings.TrimSuffix(last, ")·fm") // < Go 1.5
|
||||||
|
last = strings.TrimSuffix(last, ")-fm") // Go 1.5
|
||||||
|
last = strings.TrimSuffix(last, "·fm") // < Go 1.5
|
||||||
|
last = strings.TrimSuffix(last, "-fm") // Go 1.5
|
||||||
|
if last == "func1" { // this could mean conflicts in API docs
|
||||||
|
val := atomic.AddInt32(&anonymousFuncCount, 1)
|
||||||
|
last = "func" + fmt.Sprintf("%d", val)
|
||||||
|
atomic.StoreInt32(&anonymousFuncCount, val)
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// A RouteSelector finds the best matching Route given the input HTTP Request
|
||||||
|
// RouteSelectors can optionally also implement the PathProcessor interface to also calculate the
|
||||||
|
// path parameters after the route has been selected.
|
||||||
|
type RouteSelector interface {
|
||||||
|
|
||||||
|
// SelectRoute finds a Route given the input HTTP Request and a list of WebServices.
|
||||||
|
// It returns a selected Route and its containing WebService or an error indicating
|
||||||
|
// a problem.
|
||||||
|
SelectRoute(
|
||||||
|
webServices []*WebService,
|
||||||
|
httpRequest *http.Request) (selectedService *WebService, selected *Route, err error)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ServiceError is a transport object to pass information about a non-Http error occurred in a WebService while processing a request.
|
||||||
|
type ServiceError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError returns a ServiceError using the code and reason
|
||||||
|
func NewError(code int, message string) ServiceError {
|
||||||
|
return ServiceError{Code: code, Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a text representation of the service error
|
||||||
|
func (s ServiceError) Error() string {
|
||||||
|
return fmt.Sprintf("[ServiceError:%v] %v", s.Code, s.Message)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// WebService holds a collection of Route values that bind a Http Method + URL Path to a function.
|
||||||
|
type WebService struct {
|
||||||
|
rootPath string
|
||||||
|
pathExpr *pathExpression // cached compilation of rootPath as RegExp
|
||||||
|
routes []Route
|
||||||
|
produces []string
|
||||||
|
consumes []string
|
||||||
|
pathParameters []*Parameter
|
||||||
|
filters []FilterFunction
|
||||||
|
documentation string
|
||||||
|
apiVersion string
|
||||||
|
|
||||||
|
typeNameHandleFunc TypeNameHandleFunction
|
||||||
|
|
||||||
|
dynamicRoutes bool
|
||||||
|
|
||||||
|
// protects 'routes' if dynamic routes are enabled
|
||||||
|
routesLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebService) SetDynamicRoutes(enable bool) {
|
||||||
|
w.dynamicRoutes = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeNameHandleFunction declares functions that can handle translating the name of a sample object
|
||||||
|
// into the restful documentation for the service.
|
||||||
|
type TypeNameHandleFunction func(sample interface{}) string
|
||||||
|
|
||||||
|
// TypeNameHandler sets the function that will convert types to strings in the parameter
|
||||||
|
// and model definitions. If not set, the web service will invoke
|
||||||
|
// reflect.TypeOf(object).String().
|
||||||
|
func (w *WebService) TypeNameHandler(handler TypeNameHandleFunction) *WebService {
|
||||||
|
w.typeNameHandleFunc = handler
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// reflectTypeName is the default TypeNameHandleFunction and for a given object
|
||||||
|
// returns the name that Go identifies it with (e.g. "string" or "v1.Object") via
|
||||||
|
// the reflection API.
|
||||||
|
func reflectTypeName(sample interface{}) string {
|
||||||
|
return reflect.TypeOf(sample).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// compilePathExpression ensures that the path is compiled into a RegEx for those routers that need it.
|
||||||
|
func (w *WebService) compilePathExpression() {
|
||||||
|
compiled, err := newPathExpression(w.rootPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[restful] invalid path:%s because:%v", w.rootPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
w.pathExpr = compiled
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApiVersion sets the API version for documentation purposes.
|
||||||
|
func (w *WebService) ApiVersion(apiVersion string) *WebService {
|
||||||
|
w.apiVersion = apiVersion
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the API version for documentation purposes.
|
||||||
|
func (w *WebService) Version() string { return w.apiVersion }
|
||||||
|
|
||||||
|
// Path specifies the root URL template path of the WebService.
|
||||||
|
// All Routes will be relative to this path.
|
||||||
|
func (w *WebService) Path(root string) *WebService {
|
||||||
|
w.rootPath = root
|
||||||
|
if len(w.rootPath) == 0 {
|
||||||
|
w.rootPath = "/"
|
||||||
|
}
|
||||||
|
w.compilePathExpression()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param adds a PathParameter to document parameters used in the root path.
|
||||||
|
func (w *WebService) Param(parameter *Parameter) *WebService {
|
||||||
|
if w.pathParameters == nil {
|
||||||
|
w.pathParameters = []*Parameter{}
|
||||||
|
}
|
||||||
|
w.pathParameters = append(w.pathParameters, parameter)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameter creates a new Parameter of kind Path for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func (w *WebService) PathParameter(name, description string) *Parameter {
|
||||||
|
return PathParameter(name, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameter creates a new Parameter of kind Path for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func PathParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: true, DataType: "string"}}
|
||||||
|
p.bePath()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameter creates a new Parameter of kind Query for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func (w *WebService) QueryParameter(name, description string) *Parameter {
|
||||||
|
return QueryParameter(name, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParameter creates a new Parameter of kind Query for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func QueryParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string", CollectionFormat: CollectionFormatCSV.String()}}
|
||||||
|
p.beQuery()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyParameter creates a new Parameter of kind Body for documentation purposes.
|
||||||
|
// It is initialized as required without a DataType.
|
||||||
|
func (w *WebService) BodyParameter(name, description string) *Parameter {
|
||||||
|
return BodyParameter(name, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyParameter creates a new Parameter of kind Body for documentation purposes.
|
||||||
|
// It is initialized as required without a DataType.
|
||||||
|
func BodyParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: true}}
|
||||||
|
p.beBody()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderParameter creates a new Parameter of kind (Http) Header for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func (w *WebService) HeaderParameter(name, description string) *Parameter {
|
||||||
|
return HeaderParameter(name, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderParameter creates a new Parameter of kind (Http) Header for documentation purposes.
|
||||||
|
// It is initialized as not required with string as its DataType.
|
||||||
|
func HeaderParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string"}}
|
||||||
|
p.beHeader()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormParameter creates a new Parameter of kind Form (using application/x-www-form-urlencoded) for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func (w *WebService) FormParameter(name, description string) *Parameter {
|
||||||
|
return FormParameter(name, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormParameter creates a new Parameter of kind Form (using application/x-www-form-urlencoded) for documentation purposes.
|
||||||
|
// It is initialized as required with string as its DataType.
|
||||||
|
func FormParameter(name, description string) *Parameter {
|
||||||
|
p := &Parameter{&ParameterData{Name: name, Description: description, Required: false, DataType: "string"}}
|
||||||
|
p.beForm()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route creates a new Route using the RouteBuilder and add to the ordered list of Routes.
|
||||||
|
func (w *WebService) Route(builder *RouteBuilder) *WebService {
|
||||||
|
w.routesLock.Lock()
|
||||||
|
defer w.routesLock.Unlock()
|
||||||
|
builder.copyDefaults(w.produces, w.consumes)
|
||||||
|
w.routes = append(w.routes, builder.Build())
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoute removes the specified route, looks for something that matches 'path' and 'method'
|
||||||
|
func (w *WebService) RemoveRoute(path, method string) error {
|
||||||
|
if !w.dynamicRoutes {
|
||||||
|
return errors.New("dynamic routes are not enabled.")
|
||||||
|
}
|
||||||
|
w.routesLock.Lock()
|
||||||
|
defer w.routesLock.Unlock()
|
||||||
|
newRoutes := make([]Route, (len(w.routes) - 1))
|
||||||
|
current := 0
|
||||||
|
for ix := range w.routes {
|
||||||
|
if w.routes[ix].Method == method && w.routes[ix].Path == path {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newRoutes[current] = w.routes[ix]
|
||||||
|
current = current + 1
|
||||||
|
}
|
||||||
|
w.routes = newRoutes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method creates a new RouteBuilder and initialize its http method
|
||||||
|
func (w *WebService) Method(httpMethod string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method(httpMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produces specifies that this WebService can produce one or more MIME types.
|
||||||
|
// Http requests must have one of these values set for the Accept header.
|
||||||
|
func (w *WebService) Produces(contentTypes ...string) *WebService {
|
||||||
|
w.produces = contentTypes
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumes specifies that this WebService can consume one or more MIME types.
|
||||||
|
// Http requests must have one of these values set for the Content-Type header.
|
||||||
|
func (w *WebService) Consumes(accepts ...string) *WebService {
|
||||||
|
w.consumes = accepts
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes returns the Routes associated with this WebService
|
||||||
|
func (w *WebService) Routes() []Route {
|
||||||
|
if !w.dynamicRoutes {
|
||||||
|
return w.routes
|
||||||
|
}
|
||||||
|
// Make a copy of the array to prevent concurrency problems
|
||||||
|
w.routesLock.RLock()
|
||||||
|
defer w.routesLock.RUnlock()
|
||||||
|
result := make([]Route, len(w.routes))
|
||||||
|
for ix := range w.routes {
|
||||||
|
result[ix] = w.routes[ix]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootPath returns the RootPath associated with this WebService. Default "/"
|
||||||
|
func (w *WebService) RootPath() string {
|
||||||
|
return w.rootPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParameters return the path parameter names for (shared among its Routes)
|
||||||
|
func (w *WebService) PathParameters() []*Parameter {
|
||||||
|
return w.pathParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter adds a filter function to the chain of filters applicable to all its Routes
|
||||||
|
func (w *WebService) Filter(filter FilterFunction) *WebService {
|
||||||
|
w.filters = append(w.filters, filter)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doc is used to set the documentation of this service.
|
||||||
|
func (w *WebService) Doc(plainText string) *WebService {
|
||||||
|
w.documentation = plainText
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documentation returns it.
|
||||||
|
func (w *WebService) Documentation() string {
|
||||||
|
return w.documentation
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convenience methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
// HEAD is a shortcut for .Method("HEAD").Path(subPath)
|
||||||
|
func (w *WebService) HEAD(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("HEAD").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET is a shortcut for .Method("GET").Path(subPath)
|
||||||
|
func (w *WebService) GET(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("GET").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST is a shortcut for .Method("POST").Path(subPath)
|
||||||
|
func (w *WebService) POST(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("POST").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT is a shortcut for .Method("PUT").Path(subPath)
|
||||||
|
func (w *WebService) PUT(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("PUT").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH is a shortcut for .Method("PATCH").Path(subPath)
|
||||||
|
func (w *WebService) PATCH(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("PATCH").Path(subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE is a shortcut for .Method("DELETE").Path(subPath)
|
||||||
|
func (w *WebService) DELETE(subPath string) *RouteBuilder {
|
||||||
|
return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("DELETE").Path(subPath)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package restful
|
||||||
|
|
||||||
|
// Copyright 2013 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultContainer is a restful.Container that uses http.DefaultServeMux
|
||||||
|
var DefaultContainer *Container
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultContainer = NewContainer()
|
||||||
|
DefaultContainer.ServeMux = http.DefaultServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
// If set the true then panics will not be caught to return HTTP 500.
|
||||||
|
// In that case, Route functions are responsible for handling any error situation.
|
||||||
|
// Default value is false = recover from panics. This has performance implications.
|
||||||
|
// OBSOLETE ; use restful.DefaultContainer.DoNotRecover(true)
|
||||||
|
var DoNotRecover = false
|
||||||
|
|
||||||
|
// Add registers a new WebService add it to the DefaultContainer.
|
||||||
|
func Add(service *WebService) {
|
||||||
|
DefaultContainer.Add(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends a container FilterFunction from the DefaultContainer.
|
||||||
|
// These are called before dispatching a http.Request to a WebService.
|
||||||
|
func Filter(filter FilterFunction) {
|
||||||
|
DefaultContainer.Filter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisteredWebServices returns the collections of WebServices from the DefaultContainer
|
||||||
|
func RegisteredWebServices() []*WebService {
|
||||||
|
return DefaultContainer.RegisteredWebServices()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Sam Ghods
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// indirect walks down v allocating pointers as needed,
|
||||||
|
// until it gets to a non-pointer.
|
||||||
|
// if it encounters an Unmarshaler, indirect stops and returns that.
|
||||||
|
// if decodingNull is true, indirect stops at the last pointer so it can be set to nil.
|
||||||
|
func indirect(v reflect.Value, decodingNull bool) (json.Unmarshaler, encoding.TextUnmarshaler, reflect.Value) {
|
||||||
|
// If v is a named type and is addressable,
|
||||||
|
// start with its address, so that if the type has pointer methods,
|
||||||
|
// we find them.
|
||||||
|
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
|
||||||
|
v = v.Addr()
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
// Load value from interface, but only if the result will be
|
||||||
|
// usefully addressable.
|
||||||
|
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||||
|
e := v.Elem()
|
||||||
|
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
|
||||||
|
v = e
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Ptr {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
if v.Type().NumMethod() > 0 {
|
||||||
|
if u, ok := v.Interface().(json.Unmarshaler); ok {
|
||||||
|
return u, nil, reflect.Value{}
|
||||||
|
}
|
||||||
|
if u, ok := v.Interface().(encoding.TextUnmarshaler); ok {
|
||||||
|
return nil, u, reflect.Value{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return nil, nil, v
|
||||||
|
}
|
||||||
|
|
||||||
|
// A field represents a single field found in a struct.
|
||||||
|
type field struct {
|
||||||
|
name string
|
||||||
|
nameBytes []byte // []byte(name)
|
||||||
|
equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent
|
||||||
|
|
||||||
|
tag bool
|
||||||
|
index []int
|
||||||
|
typ reflect.Type
|
||||||
|
omitEmpty bool
|
||||||
|
quoted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillField(f field) field {
|
||||||
|
f.nameBytes = []byte(f.name)
|
||||||
|
f.equalFold = foldFunc(f.nameBytes)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// byName sorts field by name, breaking ties with depth,
|
||||||
|
// then breaking ties with "name came from json tag", then
|
||||||
|
// breaking ties with index sequence.
|
||||||
|
type byName []field
|
||||||
|
|
||||||
|
func (x byName) Len() int { return len(x) }
|
||||||
|
|
||||||
|
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||||
|
|
||||||
|
func (x byName) Less(i, j int) bool {
|
||||||
|
if x[i].name != x[j].name {
|
||||||
|
return x[i].name < x[j].name
|
||||||
|
}
|
||||||
|
if len(x[i].index) != len(x[j].index) {
|
||||||
|
return len(x[i].index) < len(x[j].index)
|
||||||
|
}
|
||||||
|
if x[i].tag != x[j].tag {
|
||||||
|
return x[i].tag
|
||||||
|
}
|
||||||
|
return byIndex(x).Less(i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
// byIndex sorts field by index sequence.
|
||||||
|
type byIndex []field
|
||||||
|
|
||||||
|
func (x byIndex) Len() int { return len(x) }
|
||||||
|
|
||||||
|
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||||
|
|
||||||
|
func (x byIndex) Less(i, j int) bool {
|
||||||
|
for k, xik := range x[i].index {
|
||||||
|
if k >= len(x[j].index) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if xik != x[j].index[k] {
|
||||||
|
return xik < x[j].index[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(x[i].index) < len(x[j].index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeFields returns a list of fields that JSON should recognize for the given type.
|
||||||
|
// The algorithm is breadth-first search over the set of structs to include - the top struct
|
||||||
|
// and then any reachable anonymous structs.
|
||||||
|
func typeFields(t reflect.Type) []field {
|
||||||
|
// Anonymous fields to explore at the current level and the next.
|
||||||
|
current := []field{}
|
||||||
|
next := []field{{typ: t}}
|
||||||
|
|
||||||
|
// Count of queued names for current level and the next.
|
||||||
|
count := map[reflect.Type]int{}
|
||||||
|
nextCount := map[reflect.Type]int{}
|
||||||
|
|
||||||
|
// Types already visited at an earlier level.
|
||||||
|
visited := map[reflect.Type]bool{}
|
||||||
|
|
||||||
|
// Fields found.
|
||||||
|
var fields []field
|
||||||
|
|
||||||
|
for len(next) > 0 {
|
||||||
|
current, next = next, current[:0]
|
||||||
|
count, nextCount = nextCount, map[reflect.Type]int{}
|
||||||
|
|
||||||
|
for _, f := range current {
|
||||||
|
if visited[f.typ] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visited[f.typ] = true
|
||||||
|
|
||||||
|
// Scan f.typ for fields to include.
|
||||||
|
for i := 0; i < f.typ.NumField(); i++ {
|
||||||
|
sf := f.typ.Field(i)
|
||||||
|
if sf.PkgPath != "" { // unexported
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tag := sf.Tag.Get("json")
|
||||||
|
if tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, opts := parseTag(tag)
|
||||||
|
if !isValidTag(name) {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
index := make([]int, len(f.index)+1)
|
||||||
|
copy(index, f.index)
|
||||||
|
index[len(f.index)] = i
|
||||||
|
|
||||||
|
ft := sf.Type
|
||||||
|
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
|
||||||
|
// Follow pointer.
|
||||||
|
ft = ft.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record found field and index sequence.
|
||||||
|
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
|
||||||
|
tagged := name != ""
|
||||||
|
if name == "" {
|
||||||
|
name = sf.Name
|
||||||
|
}
|
||||||
|
fields = append(fields, fillField(field{
|
||||||
|
name: name,
|
||||||
|
tag: tagged,
|
||||||
|
index: index,
|
||||||
|
typ: ft,
|
||||||
|
omitEmpty: opts.Contains("omitempty"),
|
||||||
|
quoted: opts.Contains("string"),
|
||||||
|
}))
|
||||||
|
if count[f.typ] > 1 {
|
||||||
|
// If there were multiple instances, add a second,
|
||||||
|
// so that the annihilation code will see a duplicate.
|
||||||
|
// It only cares about the distinction between 1 or 2,
|
||||||
|
// so don't bother generating any more copies.
|
||||||
|
fields = append(fields, fields[len(fields)-1])
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record new anonymous struct to explore in next round.
|
||||||
|
nextCount[ft]++
|
||||||
|
if nextCount[ft] == 1 {
|
||||||
|
next = append(next, fillField(field{name: ft.Name(), index: index, typ: ft}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(byName(fields))
|
||||||
|
|
||||||
|
// Delete all fields that are hidden by the Go rules for embedded fields,
|
||||||
|
// except that fields with JSON tags are promoted.
|
||||||
|
|
||||||
|
// The fields are sorted in primary order of name, secondary order
|
||||||
|
// of field index length. Loop over names; for each name, delete
|
||||||
|
// hidden fields by choosing the one dominant field that survives.
|
||||||
|
out := fields[:0]
|
||||||
|
for advance, i := 0, 0; i < len(fields); i += advance {
|
||||||
|
// One iteration per name.
|
||||||
|
// Find the sequence of fields with the name of this first field.
|
||||||
|
fi := fields[i]
|
||||||
|
name := fi.name
|
||||||
|
for advance = 1; i+advance < len(fields); advance++ {
|
||||||
|
fj := fields[i+advance]
|
||||||
|
if fj.name != name {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if advance == 1 { // Only one field with this name
|
||||||
|
out = append(out, fi)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dominant, ok := dominantField(fields[i : i+advance])
|
||||||
|
if ok {
|
||||||
|
out = append(out, dominant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = out
|
||||||
|
sort.Sort(byIndex(fields))
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// dominantField looks through the fields, all of which are known to
|
||||||
|
// have the same name, to find the single field that dominates the
|
||||||
|
// others using Go's embedding rules, modified by the presence of
|
||||||
|
// JSON tags. If there are multiple top-level fields, the boolean
|
||||||
|
// will be false: This condition is an error in Go and we skip all
|
||||||
|
// the fields.
|
||||||
|
func dominantField(fields []field) (field, bool) {
|
||||||
|
// The fields are sorted in increasing index-length order. The winner
|
||||||
|
// must therefore be one with the shortest index length. Drop all
|
||||||
|
// longer entries, which is easy: just truncate the slice.
|
||||||
|
length := len(fields[0].index)
|
||||||
|
tagged := -1 // Index of first tagged field.
|
||||||
|
for i, f := range fields {
|
||||||
|
if len(f.index) > length {
|
||||||
|
fields = fields[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if f.tag {
|
||||||
|
if tagged >= 0 {
|
||||||
|
// Multiple tagged fields at the same level: conflict.
|
||||||
|
// Return no field.
|
||||||
|
return field{}, false
|
||||||
|
}
|
||||||
|
tagged = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tagged >= 0 {
|
||||||
|
return fields[tagged], true
|
||||||
|
}
|
||||||
|
// All remaining fields have the same length. If there's more than one,
|
||||||
|
// we have a conflict (two fields named "X" at the same level) and we
|
||||||
|
// return no field.
|
||||||
|
if len(fields) > 1 {
|
||||||
|
return field{}, false
|
||||||
|
}
|
||||||
|
return fields[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldCache struct {
|
||||||
|
sync.RWMutex
|
||||||
|
m map[reflect.Type][]field
|
||||||
|
}
|
||||||
|
|
||||||
|
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
|
||||||
|
func cachedTypeFields(t reflect.Type) []field {
|
||||||
|
fieldCache.RLock()
|
||||||
|
f := fieldCache.m[t]
|
||||||
|
fieldCache.RUnlock()
|
||||||
|
if f != nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute fields without lock.
|
||||||
|
// Might duplicate effort but won't hold other computations back.
|
||||||
|
f = typeFields(t)
|
||||||
|
if f == nil {
|
||||||
|
f = []field{}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldCache.Lock()
|
||||||
|
if fieldCache.m == nil {
|
||||||
|
fieldCache.m = map[reflect.Type][]field{}
|
||||||
|
}
|
||||||
|
fieldCache.m[t] = f
|
||||||
|
fieldCache.Unlock()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidTag(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range s {
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
|
||||||
|
// Backslash and quote chars are reserved, but
|
||||||
|
// otherwise any punctuation chars are allowed
|
||||||
|
// in a tag name.
|
||||||
|
default:
|
||||||
|
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
|
||||||
|
kelvin = '\u212a'
|
||||||
|
smallLongEss = '\u017f'
|
||||||
|
)
|
||||||
|
|
||||||
|
// foldFunc returns one of four different case folding equivalence
|
||||||
|
// functions, from most general (and slow) to fastest:
|
||||||
|
//
|
||||||
|
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
|
||||||
|
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
|
||||||
|
// 3) asciiEqualFold, no special, but includes non-letters (including _)
|
||||||
|
// 4) simpleLetterEqualFold, no specials, no non-letters.
|
||||||
|
//
|
||||||
|
// The letters S and K are special because they map to 3 runes, not just 2:
|
||||||
|
// * S maps to s and to U+017F 'ſ' Latin small letter long s
|
||||||
|
// * k maps to K and to U+212A 'K' Kelvin sign
|
||||||
|
// See http://play.golang.org/p/tTxjOc0OGo
|
||||||
|
//
|
||||||
|
// The returned function is specialized for matching against s and
|
||||||
|
// should only be given s. It's not curried for performance reasons.
|
||||||
|
func foldFunc(s []byte) func(s, t []byte) bool {
|
||||||
|
nonLetter := false
|
||||||
|
special := false // special letter
|
||||||
|
for _, b := range s {
|
||||||
|
if b >= utf8.RuneSelf {
|
||||||
|
return bytes.EqualFold
|
||||||
|
}
|
||||||
|
upper := b & caseMask
|
||||||
|
if upper < 'A' || upper > 'Z' {
|
||||||
|
nonLetter = true
|
||||||
|
} else if upper == 'K' || upper == 'S' {
|
||||||
|
// See above for why these letters are special.
|
||||||
|
special = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if special {
|
||||||
|
return equalFoldRight
|
||||||
|
}
|
||||||
|
if nonLetter {
|
||||||
|
return asciiEqualFold
|
||||||
|
}
|
||||||
|
return simpleLetterEqualFold
|
||||||
|
}
|
||||||
|
|
||||||
|
// equalFoldRight is a specialization of bytes.EqualFold when s is
|
||||||
|
// known to be all ASCII (including punctuation), but contains an 's',
|
||||||
|
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
|
||||||
|
// See comments on foldFunc.
|
||||||
|
func equalFoldRight(s, t []byte) bool {
|
||||||
|
for _, sb := range s {
|
||||||
|
if len(t) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tb := t[0]
|
||||||
|
if tb < utf8.RuneSelf {
|
||||||
|
if sb != tb {
|
||||||
|
sbUpper := sb & caseMask
|
||||||
|
if 'A' <= sbUpper && sbUpper <= 'Z' {
|
||||||
|
if sbUpper != tb&caseMask {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = t[1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// sb is ASCII and t is not. t must be either kelvin
|
||||||
|
// sign or long s; sb must be s, S, k, or K.
|
||||||
|
tr, size := utf8.DecodeRune(t)
|
||||||
|
switch sb {
|
||||||
|
case 's', 'S':
|
||||||
|
if tr != smallLongEss {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case 'k', 'K':
|
||||||
|
if tr != kelvin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t = t[size:]
|
||||||
|
|
||||||
|
}
|
||||||
|
if len(t) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiEqualFold is a specialization of bytes.EqualFold for use when
|
||||||
|
// s is all ASCII (but may contain non-letters) and contains no
|
||||||
|
// special-folding letters.
|
||||||
|
// See comments on foldFunc.
|
||||||
|
func asciiEqualFold(s, t []byte) bool {
|
||||||
|
if len(s) != len(t) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, sb := range s {
|
||||||
|
tb := t[i]
|
||||||
|
if sb == tb {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
|
||||||
|
if sb&caseMask != tb&caseMask {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
|
||||||
|
// use when s is all ASCII letters (no underscores, etc) and also
|
||||||
|
// doesn't contain 'k', 'K', 's', or 'S'.
|
||||||
|
// See comments on foldFunc.
|
||||||
|
func simpleLetterEqualFold(s, t []byte) bool {
|
||||||
|
if len(s) != len(t) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, b := range s {
|
||||||
|
if b&caseMask != t[i]&caseMask {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagOptions is the string following a comma in a struct field's "json"
|
||||||
|
// tag, or the empty string. It does not include the leading comma.
|
||||||
|
type tagOptions string
|
||||||
|
|
||||||
|
// parseTag splits a struct field's json tag into its name and
|
||||||
|
// comma-separated options.
|
||||||
|
func parseTag(tag string) (string, tagOptions) {
|
||||||
|
if idx := strings.Index(tag, ","); idx != -1 {
|
||||||
|
return tag[:idx], tagOptions(tag[idx+1:])
|
||||||
|
}
|
||||||
|
return tag, tagOptions("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains reports whether a comma-separated list of options
|
||||||
|
// contains a particular substr flag. substr must be surrounded by a
|
||||||
|
// string boundary or commas.
|
||||||
|
func (o tagOptions) Contains(optionName string) bool {
|
||||||
|
if len(o) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s := string(o)
|
||||||
|
for s != "" {
|
||||||
|
var next string
|
||||||
|
i := strings.Index(s, ",")
|
||||||
|
if i >= 0 {
|
||||||
|
s, next = s[:i], s[i+1:]
|
||||||
|
}
|
||||||
|
if s == optionName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s = next
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
package yaml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshals the object into JSON then converts JSON to YAML and returns the
|
||||||
|
// YAML.
|
||||||
|
func Marshal(o interface{}) ([]byte, error) {
|
||||||
|
j, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshaling into JSON: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
y, err := JSONToYAML(j)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error converting JSON to YAML: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return y, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts YAML to JSON then uses JSON to unmarshal into an object.
|
||||||
|
func Unmarshal(y []byte, o interface{}) error {
|
||||||
|
vo := reflect.ValueOf(o)
|
||||||
|
j, err := yamlToJSON(y, &vo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting YAML to JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(j, o)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error unmarshaling JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JSON to YAML.
|
||||||
|
func JSONToYAML(j []byte) ([]byte, error) {
|
||||||
|
// Convert the JSON to an object.
|
||||||
|
var jsonObj interface{}
|
||||||
|
// We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
|
||||||
|
// Go JSON library doesn't try to pick the right number type (int, float,
|
||||||
|
// etc.) when unmarshling to interface{}, it just picks float64
|
||||||
|
// universally. go-yaml does go through the effort of picking the right
|
||||||
|
// number type, so we can preserve number type throughout this process.
|
||||||
|
err := yaml.Unmarshal(j, &jsonObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal this object into YAML.
|
||||||
|
return yaml.Marshal(jsonObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert YAML to JSON. Since JSON is a subset of YAML, passing JSON through
|
||||||
|
// this method should be a no-op.
|
||||||
|
//
|
||||||
|
// Things YAML can do that are not supported by JSON:
|
||||||
|
// * In YAML you can have binary and null keys in your maps. These are invalid
|
||||||
|
// in JSON. (int and float keys are converted to strings.)
|
||||||
|
// * Binary data in YAML with the !!binary tag is not supported. If you want to
|
||||||
|
// use binary data with this library, encode the data as base64 as usual but do
|
||||||
|
// not use the !!binary tag in your YAML. This will ensure the original base64
|
||||||
|
// encoded data makes it all the way through to the JSON.
|
||||||
|
func YAMLToJSON(y []byte) ([]byte, error) {
|
||||||
|
return yamlToJSON(y, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func yamlToJSON(y []byte, jsonTarget *reflect.Value) ([]byte, error) {
|
||||||
|
// Convert the YAML to an object.
|
||||||
|
var yamlObj interface{}
|
||||||
|
err := yaml.Unmarshal(y, &yamlObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAML objects are not completely compatible with JSON objects (e.g. you
|
||||||
|
// can have non-string keys in YAML). So, convert the YAML-compatible object
|
||||||
|
// to a JSON-compatible object, failing with an error if irrecoverable
|
||||||
|
// incompatibilties happen along the way.
|
||||||
|
jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert this object to JSON and return the data.
|
||||||
|
return json.Marshal(jsonObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Resolve jsonTarget to a concrete value (i.e. not a pointer or an
|
||||||
|
// interface). We pass decodingNull as false because we're not actually
|
||||||
|
// decoding into the value, we're just checking if the ultimate target is a
|
||||||
|
// string.
|
||||||
|
if jsonTarget != nil {
|
||||||
|
ju, tu, pv := indirect(*jsonTarget, false)
|
||||||
|
// We have a JSON or Text Umarshaler at this level, so we can't be trying
|
||||||
|
// to decode into a string.
|
||||||
|
if ju != nil || tu != nil {
|
||||||
|
jsonTarget = nil
|
||||||
|
} else {
|
||||||
|
jsonTarget = &pv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If yamlObj is a number or a boolean, check if jsonTarget is a string -
|
||||||
|
// if so, coerce. Else return normal.
|
||||||
|
// If yamlObj is a map or array, find the field that each key is
|
||||||
|
// unmarshaling to, and when you recurse pass the reflect.Value for that
|
||||||
|
// field back into this function.
|
||||||
|
switch typedYAMLObj := yamlObj.(type) {
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
// JSON does not support arbitrary keys in a map, so we must convert
|
||||||
|
// these keys to strings.
|
||||||
|
//
|
||||||
|
// From my reading of go-yaml v2 (specifically the resolve function),
|
||||||
|
// keys can only have the types string, int, int64, float64, binary
|
||||||
|
// (unsupported), or null (unsupported).
|
||||||
|
strMap := make(map[string]interface{})
|
||||||
|
for k, v := range typedYAMLObj {
|
||||||
|
// Resolve the key to a string first.
|
||||||
|
var keyString string
|
||||||
|
switch typedKey := k.(type) {
|
||||||
|
case string:
|
||||||
|
keyString = typedKey
|
||||||
|
case int:
|
||||||
|
keyString = strconv.Itoa(typedKey)
|
||||||
|
case int64:
|
||||||
|
// go-yaml will only return an int64 as a key if the system
|
||||||
|
// architecture is 32-bit and the key's value is between 32-bit
|
||||||
|
// and 64-bit. Otherwise the key type will simply be int.
|
||||||
|
keyString = strconv.FormatInt(typedKey, 10)
|
||||||
|
case float64:
|
||||||
|
// Stolen from go-yaml to use the same conversion to string as
|
||||||
|
// the go-yaml library uses to convert float to string when
|
||||||
|
// Marshaling.
|
||||||
|
s := strconv.FormatFloat(typedKey, 'g', -1, 32)
|
||||||
|
switch s {
|
||||||
|
case "+Inf":
|
||||||
|
s = ".inf"
|
||||||
|
case "-Inf":
|
||||||
|
s = "-.inf"
|
||||||
|
case "NaN":
|
||||||
|
s = ".nan"
|
||||||
|
}
|
||||||
|
keyString = s
|
||||||
|
case bool:
|
||||||
|
if typedKey {
|
||||||
|
keyString = "true"
|
||||||
|
} else {
|
||||||
|
keyString = "false"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unsupported map key of type: %s, key: %+#v, value: %+#v",
|
||||||
|
reflect.TypeOf(k), k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonTarget should be a struct or a map. If it's a struct, find
|
||||||
|
// the field it's going to map to and pass its reflect.Value. If
|
||||||
|
// it's a map, find the element type of the map and pass the
|
||||||
|
// reflect.Value created from that type. If it's neither, just pass
|
||||||
|
// nil - JSON conversion will error for us if it's a real issue.
|
||||||
|
if jsonTarget != nil {
|
||||||
|
t := *jsonTarget
|
||||||
|
if t.Kind() == reflect.Struct {
|
||||||
|
keyBytes := []byte(keyString)
|
||||||
|
// Find the field that the JSON library would use.
|
||||||
|
var f *field
|
||||||
|
fields := cachedTypeFields(t.Type())
|
||||||
|
for i := range fields {
|
||||||
|
ff := &fields[i]
|
||||||
|
if bytes.Equal(ff.nameBytes, keyBytes) {
|
||||||
|
f = ff
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Do case-insensitive comparison.
|
||||||
|
if f == nil && ff.equalFold(ff.nameBytes, keyBytes) {
|
||||||
|
f = ff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f != nil {
|
||||||
|
// Find the reflect.Value of the most preferential
|
||||||
|
// struct field.
|
||||||
|
jtf := t.Field(f.index[0])
|
||||||
|
strMap[keyString], err = convertToJSONableObject(v, &jtf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if t.Kind() == reflect.Map {
|
||||||
|
// Create a zero value of the map's element type to use as
|
||||||
|
// the JSON target.
|
||||||
|
jtv := reflect.Zero(t.Type().Elem())
|
||||||
|
strMap[keyString], err = convertToJSONableObject(v, &jtv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strMap[keyString], err = convertToJSONableObject(v, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strMap, nil
|
||||||
|
case []interface{}:
|
||||||
|
// We need to recurse into arrays in case there are any
|
||||||
|
// map[interface{}]interface{}'s inside and to convert any
|
||||||
|
// numbers to strings.
|
||||||
|
|
||||||
|
// If jsonTarget is a slice (which it really should be), find the
|
||||||
|
// thing it's going to map to. If it's not a slice, just pass nil
|
||||||
|
// - JSON conversion will error for us if it's a real issue.
|
||||||
|
var jsonSliceElemValue *reflect.Value
|
||||||
|
if jsonTarget != nil {
|
||||||
|
t := *jsonTarget
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
// By default slices point to nil, but we need a reflect.Value
|
||||||
|
// pointing to a value of the slice type, so we create one here.
|
||||||
|
ev := reflect.Indirect(reflect.New(t.Type().Elem()))
|
||||||
|
jsonSliceElemValue = &ev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make and use a new array.
|
||||||
|
arr := make([]interface{}, len(typedYAMLObj))
|
||||||
|
for i, v := range typedYAMLObj {
|
||||||
|
arr[i], err = convertToJSONableObject(v, jsonSliceElemValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr, nil
|
||||||
|
default:
|
||||||
|
// If the target type is a string and the YAML type is a number,
|
||||||
|
// convert the YAML type to a string.
|
||||||
|
if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String {
|
||||||
|
// Based on my reading of go-yaml, it may return int, int64,
|
||||||
|
// float64, or uint64.
|
||||||
|
var s string
|
||||||
|
switch typedVal := typedYAMLObj.(type) {
|
||||||
|
case int:
|
||||||
|
s = strconv.FormatInt(int64(typedVal), 10)
|
||||||
|
case int64:
|
||||||
|
s = strconv.FormatInt(typedVal, 10)
|
||||||
|
case float64:
|
||||||
|
s = strconv.FormatFloat(typedVal, 'g', -1, 32)
|
||||||
|
case uint64:
|
||||||
|
s = strconv.FormatUint(typedVal, 10)
|
||||||
|
case bool:
|
||||||
|
if typedVal {
|
||||||
|
s = "true"
|
||||||
|
} else {
|
||||||
|
s = "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s) > 0 {
|
||||||
|
yamlObj = interface{}(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yamlObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
@ -0,0 +1,390 @@
|
||||||
|
// Copyright 2013 sigu-399 ( https://github.com/sigu-399 )
|
||||||
|
//
|
||||||
|
// 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 sigu-399
|
||||||
|
// author-github https://github.com/sigu-399
|
||||||
|
// author-mail sigu.399@gmail.com
|
||||||
|
//
|
||||||
|
// repository-name jsonpointer
|
||||||
|
// repository-desc An implementation of JSON Pointer - Go language
|
||||||
|
//
|
||||||
|
// description Main and unique file.
|
||||||
|
//
|
||||||
|
// created 25-02-2013
|
||||||
|
|
||||||
|
package jsonpointer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-openapi/swag"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
emptyPointer = ``
|
||||||
|
pointerSeparator = `/`
|
||||||
|
|
||||||
|
invalidStart = `JSON pointer must be empty or start with a "` + pointerSeparator
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsonPointableType = reflect.TypeOf(new(JSONPointable)).Elem()
|
||||||
|
var jsonSetableType = reflect.TypeOf(new(JSONSetable)).Elem()
|
||||||
|
|
||||||
|
// JSONPointable is an interface for structs to implement when they need to customize the
|
||||||
|
// json pointer process
|
||||||
|
type JSONPointable interface {
|
||||||
|
JSONLookup(string) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONSetable is an interface for structs to implement when they need to customize the
|
||||||
|
// json pointer process
|
||||||
|
type JSONSetable interface {
|
||||||
|
JSONSet(string, interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new json pointer for the given string
|
||||||
|
func New(jsonPointerString string) (Pointer, error) {
|
||||||
|
|
||||||
|
var p Pointer
|
||||||
|
err := p.parse(jsonPointerString)
|
||||||
|
return p, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer the json pointer reprsentation
|
||||||
|
type Pointer struct {
|
||||||
|
referenceTokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Constructor", parses the given string JSON pointer
|
||||||
|
func (p *Pointer) parse(jsonPointerString string) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if jsonPointerString != emptyPointer {
|
||||||
|
if !strings.HasPrefix(jsonPointerString, pointerSeparator) {
|
||||||
|
err = errors.New(invalidStart)
|
||||||
|
} else {
|
||||||
|
referenceTokens := strings.Split(jsonPointerString, pointerSeparator)
|
||||||
|
for _, referenceToken := range referenceTokens[1:] {
|
||||||
|
p.referenceTokens = append(p.referenceTokens, referenceToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get uses the pointer to retrieve a value from a JSON document
|
||||||
|
func (p *Pointer) Get(document interface{}) (interface{}, reflect.Kind, error) {
|
||||||
|
return p.get(document, swag.DefaultJSONNameProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uses the pointer to set a value from a JSON document
|
||||||
|
func (p *Pointer) Set(document interface{}, value interface{}) (interface{}, error) {
|
||||||
|
return document, p.set(document, value, swag.DefaultJSONNameProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetForToken gets a value for a json pointer token 1 level deep
|
||||||
|
func GetForToken(document interface{}, decodedToken string) (interface{}, reflect.Kind, error) {
|
||||||
|
return getSingleImpl(document, decodedToken, swag.DefaultJSONNameProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetForToken gets a value for a json pointer token 1 level deep
|
||||||
|
func SetForToken(document interface{}, decodedToken string, value interface{}) (interface{}, error) {
|
||||||
|
return document, setSingleImpl(document, value, decodedToken, swag.DefaultJSONNameProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSingleImpl(node interface{}, decodedToken string, nameProvider *swag.NameProvider) (interface{}, reflect.Kind, error) {
|
||||||
|
rValue := reflect.Indirect(reflect.ValueOf(node))
|
||||||
|
kind := rValue.Kind()
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
if rValue.Type().Implements(jsonPointableType) {
|
||||||
|
r, err := node.(JSONPointable).JSONLookup(decodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, kind, err
|
||||||
|
}
|
||||||
|
return r, kind, nil
|
||||||
|
}
|
||||||
|
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
|
||||||
|
if !ok {
|
||||||
|
return nil, kind, fmt.Errorf("object has no field %q", decodedToken)
|
||||||
|
}
|
||||||
|
fld := rValue.FieldByName(nm)
|
||||||
|
return fld.Interface(), kind, nil
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
kv := reflect.ValueOf(decodedToken)
|
||||||
|
mv := rValue.MapIndex(kv)
|
||||||
|
|
||||||
|
if mv.IsValid() && !swag.IsZero(mv) {
|
||||||
|
return mv.Interface(), kind, nil
|
||||||
|
}
|
||||||
|
return nil, kind, fmt.Errorf("object has no key %q", decodedToken)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
tokenIndex, err := strconv.Atoi(decodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, kind, err
|
||||||
|
}
|
||||||
|
sLength := rValue.Len()
|
||||||
|
if tokenIndex < 0 || tokenIndex >= sLength {
|
||||||
|
return nil, kind, fmt.Errorf("index out of bounds array[0,%d] index '%d'", sLength-1, tokenIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := rValue.Index(tokenIndex)
|
||||||
|
return elem.Interface(), kind, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, kind, fmt.Errorf("invalid token reference %q", decodedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSingleImpl(node, data interface{}, decodedToken string, nameProvider *swag.NameProvider) error {
|
||||||
|
rValue := reflect.Indirect(reflect.ValueOf(node))
|
||||||
|
switch rValue.Kind() {
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
if ns, ok := node.(JSONSetable); ok { // pointer impl
|
||||||
|
return ns.JSONSet(decodedToken, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rValue.Type().Implements(jsonSetableType) {
|
||||||
|
return node.(JSONSetable).JSONSet(decodedToken, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("object has no field %q", decodedToken)
|
||||||
|
}
|
||||||
|
fld := rValue.FieldByName(nm)
|
||||||
|
if fld.IsValid() {
|
||||||
|
fld.Set(reflect.ValueOf(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
kv := reflect.ValueOf(decodedToken)
|
||||||
|
rValue.SetMapIndex(kv, reflect.ValueOf(data))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
tokenIndex, err := strconv.Atoi(decodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sLength := rValue.Len()
|
||||||
|
if tokenIndex < 0 || tokenIndex >= sLength {
|
||||||
|
return fmt.Errorf("index out of bounds array[0,%d] index '%d'", sLength, tokenIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := rValue.Index(tokenIndex)
|
||||||
|
if !elem.CanSet() {
|
||||||
|
return fmt.Errorf("can't set slice index %s to %v", decodedToken, data)
|
||||||
|
}
|
||||||
|
elem.Set(reflect.ValueOf(data))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid token reference %q", decodedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pointer) get(node interface{}, nameProvider *swag.NameProvider) (interface{}, reflect.Kind, error) {
|
||||||
|
|
||||||
|
if nameProvider == nil {
|
||||||
|
nameProvider = swag.DefaultJSONNameProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
kind := reflect.Invalid
|
||||||
|
|
||||||
|
// Full document when empty
|
||||||
|
if len(p.referenceTokens) == 0 {
|
||||||
|
return node, kind, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range p.referenceTokens {
|
||||||
|
|
||||||
|
decodedToken := Unescape(token)
|
||||||
|
|
||||||
|
r, knd, err := getSingleImpl(node, decodedToken, nameProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, knd, err
|
||||||
|
}
|
||||||
|
node, kind = r, knd
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
rValue := reflect.ValueOf(node)
|
||||||
|
kind = rValue.Kind()
|
||||||
|
|
||||||
|
return node, kind, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pointer) set(node, data interface{}, nameProvider *swag.NameProvider) error {
|
||||||
|
knd := reflect.ValueOf(node).Kind()
|
||||||
|
|
||||||
|
if knd != reflect.Ptr && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array {
|
||||||
|
return fmt.Errorf("only structs, pointers, maps and slices are supported for setting values")
|
||||||
|
}
|
||||||
|
|
||||||
|
if nameProvider == nil {
|
||||||
|
nameProvider = swag.DefaultJSONNameProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full document when empty
|
||||||
|
if len(p.referenceTokens) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastI := len(p.referenceTokens) - 1
|
||||||
|
for i, token := range p.referenceTokens {
|
||||||
|
isLastToken := i == lastI
|
||||||
|
decodedToken := Unescape(token)
|
||||||
|
|
||||||
|
if isLastToken {
|
||||||
|
|
||||||
|
return setSingleImpl(node, data, decodedToken, nameProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
rValue := reflect.Indirect(reflect.ValueOf(node))
|
||||||
|
kind := rValue.Kind()
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
if rValue.Type().Implements(jsonPointableType) {
|
||||||
|
r, err := node.(JSONPointable).JSONLookup(decodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fld := reflect.ValueOf(r)
|
||||||
|
if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Ptr {
|
||||||
|
node = fld.Addr().Interface()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = r
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("object has no field %q", decodedToken)
|
||||||
|
}
|
||||||
|
fld := rValue.FieldByName(nm)
|
||||||
|
if fld.CanAddr() && fld.Kind() != reflect.Interface && fld.Kind() != reflect.Map && fld.Kind() != reflect.Slice && fld.Kind() != reflect.Ptr {
|
||||||
|
node = fld.Addr().Interface()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = fld.Interface()
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
kv := reflect.ValueOf(decodedToken)
|
||||||
|
mv := rValue.MapIndex(kv)
|
||||||
|
|
||||||
|
if !mv.IsValid() {
|
||||||
|
return fmt.Errorf("object has no key %q", decodedToken)
|
||||||
|
}
|
||||||
|
if mv.CanAddr() && mv.Kind() != reflect.Interface && mv.Kind() != reflect.Map && mv.Kind() != reflect.Slice && mv.Kind() != reflect.Ptr {
|
||||||
|
node = mv.Addr().Interface()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = mv.Interface()
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
tokenIndex, err := strconv.Atoi(decodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sLength := rValue.Len()
|
||||||
|
if tokenIndex < 0 || tokenIndex >= sLength {
|
||||||
|
return fmt.Errorf("index out of bounds array[0,%d] index '%d'", sLength, tokenIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := rValue.Index(tokenIndex)
|
||||||
|
if elem.CanAddr() && elem.Kind() != reflect.Interface && elem.Kind() != reflect.Map && elem.Kind() != reflect.Slice && elem.Kind() != reflect.Ptr {
|
||||||
|
node = elem.Addr().Interface()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node = elem.Interface()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid token reference %q", decodedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodedTokens returns the decoded tokens
|
||||||
|
func (p *Pointer) DecodedTokens() []string {
|
||||||
|
result := make([]string, 0, len(p.referenceTokens))
|
||||||
|
for _, t := range p.referenceTokens {
|
||||||
|
result = append(result, Unescape(t))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if this is an empty json pointer
|
||||||
|
// this indicates that it points to the root document
|
||||||
|
func (p *Pointer) IsEmpty() bool {
|
||||||
|
return len(p.referenceTokens) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer to string representation function
|
||||||
|
func (p *Pointer) String() string {
|
||||||
|
|
||||||
|
if len(p.referenceTokens) == 0 {
|
||||||
|
return emptyPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerString := pointerSeparator + strings.Join(p.referenceTokens, pointerSeparator)
|
||||||
|
|
||||||
|
return pointerString
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific JSON pointer encoding here
|
||||||
|
// ~0 => ~
|
||||||
|
// ~1 => /
|
||||||
|
// ... and vice versa
|
||||||
|
|
||||||
|
const (
|
||||||
|
encRefTok0 = `~0`
|
||||||
|
encRefTok1 = `~1`
|
||||||
|
decRefTok0 = `~`
|
||||||
|
decRefTok1 = `/`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unescape unescapes a json pointer reference token string to the original representation
|
||||||
|
func Unescape(token string) string {
|
||||||
|
step1 := strings.Replace(token, encRefTok1, decRefTok1, -1)
|
||||||
|
step2 := strings.Replace(step1, encRefTok0, decRefTok0, -1)
|
||||||
|
return step2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape escapes a pointer reference token string
|
||||||
|
func Escape(token string) string {
|
||||||
|
step1 := strings.Replace(token, decRefTok0, encRefTok0, -1)
|
||||||
|
step2 := strings.Replace(step1, decRefTok1, encRefTok1, -1)
|
||||||
|
return step2
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
// Copyright 2013 sigu-399 ( https://github.com/sigu-399 )
|
||||||
|
//
|
||||||
|
// 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 sigu-399
|
||||||
|
// author-github https://github.com/sigu-399
|
||||||
|
// author-mail sigu.399@gmail.com
|
||||||
|
//
|
||||||
|
// repository-name jsonreference
|
||||||
|
// repository-desc An implementation of JSON Reference - Go language
|
||||||
|
//
|
||||||
|
// description Main and unique file.
|
||||||
|
//
|
||||||
|
// created 26-02-2013
|
||||||
|
|
||||||
|
package jsonreference
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/purell"
|
||||||
|
"github.com/go-openapi/jsonpointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fragmentRune = `#`
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new reference for the given string
|
||||||
|
func New(jsonReferenceString string) (Ref, error) {
|
||||||
|
|
||||||
|
var r Ref
|
||||||
|
err := r.parse(jsonReferenceString)
|
||||||
|
return r, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCreateRef parses the ref string and panics when it's invalid.
|
||||||
|
// Use the New method for a version that returns an error
|
||||||
|
func MustCreateRef(ref string) Ref {
|
||||||
|
r, err := New(ref)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref represents a json reference object
|
||||||
|
type Ref struct {
|
||||||
|
referenceURL *url.URL
|
||||||
|
referencePointer jsonpointer.Pointer
|
||||||
|
|
||||||
|
HasFullURL bool
|
||||||
|
HasURLPathOnly bool
|
||||||
|
HasFragmentOnly bool
|
||||||
|
HasFileScheme bool
|
||||||
|
HasFullFilePath bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURL gets the URL for this reference
|
||||||
|
func (r *Ref) GetURL() *url.URL {
|
||||||
|
return r.referenceURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointer gets the json pointer for this reference
|
||||||
|
func (r *Ref) GetPointer() *jsonpointer.Pointer {
|
||||||
|
return &r.referencePointer
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the best version of the url for this reference
|
||||||
|
func (r *Ref) String() string {
|
||||||
|
|
||||||
|
if r.referenceURL != nil {
|
||||||
|
return r.referenceURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.HasFragmentOnly {
|
||||||
|
return fragmentRune + r.referencePointer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.referencePointer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRoot returns true if this reference is a root document
|
||||||
|
func (r *Ref) IsRoot() bool {
|
||||||
|
return r.referenceURL != nil &&
|
||||||
|
!r.IsCanonical() &&
|
||||||
|
!r.HasURLPathOnly &&
|
||||||
|
r.referenceURL.Fragment == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCanonical returns true when this pointer starts with http(s):// or file://
|
||||||
|
func (r *Ref) IsCanonical() bool {
|
||||||
|
return (r.HasFileScheme && r.HasFullFilePath) || (!r.HasFileScheme && r.HasFullURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Constructor", parses the given string JSON reference
|
||||||
|
func (r *Ref) parse(jsonReferenceString string) error {
|
||||||
|
|
||||||
|
parsed, err := url.Parse(jsonReferenceString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.referenceURL, _ = url.Parse(purell.NormalizeURL(parsed, purell.FlagsSafe|purell.FlagRemoveDuplicateSlashes))
|
||||||
|
refURL := r.referenceURL
|
||||||
|
|
||||||
|
if refURL.Scheme != "" && refURL.Host != "" {
|
||||||
|
r.HasFullURL = true
|
||||||
|
} else {
|
||||||
|
if refURL.Path != "" {
|
||||||
|
r.HasURLPathOnly = true
|
||||||
|
} else if refURL.RawQuery == "" && refURL.Fragment != "" {
|
||||||
|
r.HasFragmentOnly = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.HasFileScheme = refURL.Scheme == "file"
|
||||||
|
r.HasFullFilePath = strings.HasPrefix(refURL.Path, "/")
|
||||||
|
|
||||||
|
// invalid json-pointer error means url has no json-pointer fragment. simply ignore error
|
||||||
|
r.referencePointer, _ = jsonpointer.New(refURL.Fragment)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inherits creates a new reference from a parent and a child
|
||||||
|
// If the child cannot inherit from the parent, an error is returned
|
||||||
|
func (r *Ref) Inherits(child Ref) (*Ref, error) {
|
||||||
|
childURL := child.GetURL()
|
||||||
|
parentURL := r.GetURL()
|
||||||
|
if childURL == nil {
|
||||||
|
return nil, errors.New("child url is nil")
|
||||||
|
}
|
||||||
|
if parentURL == nil {
|
||||||
|
return &child, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := New(parentURL.ResolveReference(childURL).String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ref, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue