Initial commit
This commit is contained in:
24
.circleci/config.yml
Normal file
24
.circleci/config.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
# specify the version
|
||||
- image: circleci/golang:1.9
|
||||
|
||||
working_directory: /go/src/github.com/virtual-kubelet/virtual-kubelet
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Create the credentials file
|
||||
command: sh scripts/createCredentials.sh
|
||||
- run:
|
||||
name: Build and deploy connector
|
||||
command: sh scripts/envCreation.sh
|
||||
- run: |
|
||||
echo 'export AZURE_AUTH_LOCATION=${outputPathCredsfile}' >> $BASH_ENV
|
||||
- run:
|
||||
name: Dependencies
|
||||
command: go get -v -t -d ./...
|
||||
- run:
|
||||
name: Tests
|
||||
command: go test -v ./...
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
bin/
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
/bin
|
||||
/dist
|
||||
/build
|
||||
/cover
|
||||
/test
|
||||
|
||||
# Test credentials file
|
||||
credentials.json
|
||||
43
.goreleaser.yml
Normal file
43
.goreleaser.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
project_name: virtual-kubelet
|
||||
release:
|
||||
github:
|
||||
owner: bketelsen
|
||||
name: virtual-kubelet
|
||||
name_template: '{{.Tag}}'
|
||||
brew:
|
||||
commit_author:
|
||||
name: virtual-kubelet
|
||||
email: bketelsen@gmail.com
|
||||
install: bin.install "virtual-kubelet"
|
||||
builds:
|
||||
- goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
main: .
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.BuildDate={{.Date}}
|
||||
binary: newgo
|
||||
archive:
|
||||
format: tar.gz
|
||||
name_template: '{{.Binary}}_{{.Version}}_{{.Os}}_{{.Arch }}{{if .Arm}}v{{.Arm }}{{end }}'
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
- license*
|
||||
- LICENSE*
|
||||
- readme*
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
fpm:
|
||||
bindir: /usr/local/bin
|
||||
snapshot:
|
||||
name_template: SNAPSHOT-{{.Commit }}
|
||||
checksum:
|
||||
name_template: '{{.ProjectName }}_{{.Version }}_checksums.txt'
|
||||
|
||||
6
AUTHORS
Normal file
6
AUTHORS
Normal file
@@ -0,0 +1,6 @@
|
||||
Brian Ketelsen <bketelsen@gmail.com>
|
||||
Erik St. Martin <alakriti@gmail.com>
|
||||
Jess Frazelle <acidburn@microsoft.com>
|
||||
Julien Stroheker <julienstroheker@gmail.com>
|
||||
Ria Bhatia <ria.bhatia@microsoft.com>
|
||||
Rita Zhang <rita.z.zhang@gmail.com>
|
||||
107
CONTRIBUTING.md
Normal file
107
CONTRIBUTING.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
The Virtual Kubelet accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution
|
||||
accepted.
|
||||
|
||||
## Contributor License Agreements
|
||||
|
||||
**Azure Specific**
|
||||
|
||||
If you are providing provider support for Azure then we have to jump through some legal hurdles first.
|
||||
|
||||
The [Microsoft CLA](https://cla.microsoft.com/) must be signed by all
|
||||
contributors. Please fill out either the individual or corporate Contributor
|
||||
License Agreement (CLA). Once you are CLA'ed, we'll be able to accept your pull
|
||||
requests.
|
||||
|
||||
***NOTE***: Only original source code from you and other people that have
|
||||
signed the CLA can be accepted into the repository.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Each provider is responsible for reviewing PRs. Each provider has a primary and secondary maintainer for the purposes of maintaining their own code.
|
||||
Here's the current list of maintainers.
|
||||
|
||||
Otherwise for the primary Virtual Kubelet code, and overall project maintenance, these are the current maintainers. If you want to become a maintainer for the overall project please email ria.bhatia@microsoft.com.
|
||||
|
||||
### Overall Maintainers
|
||||
|
||||
Ria Bhatia (ribhatia@microsoft.com)
|
||||
|
||||
Eric St. Martin (st.erik@microsoft.com)
|
||||
|
||||
Robbie Zhang (junjiez@microsoft.com)
|
||||
|
||||
### Provider maintainers
|
||||
|
||||
**Azure**
|
||||
|
||||
Eric St. Martin (st.erik@microsoft.com)
|
||||
|
||||
Robbie Zhang (junjiez@microsoft.com)
|
||||
|
||||
**Hyper.sh**
|
||||
|
||||
Harry Zhang (harryzhang@zju.edu.cn)
|
||||
|
||||
## Support Channels
|
||||
|
||||
This is an open source project and as such no formal support is available.
|
||||
However, like all good open source projects we do offer "best effort" support
|
||||
through [github issues](https://github.com/virtual-kubelet/virtual-kubelet).
|
||||
|
||||
Before opening a new issue or submitting a new pull request, it's helpful to
|
||||
search the project - it's likely that another user has already reported the
|
||||
issue you're facing, or it's a known issue that we're already aware of.
|
||||
|
||||
## Issues
|
||||
|
||||
Issues are used as the primary method for tracking anything to do with the
|
||||
Virtual Kubelet.
|
||||
|
||||
### Issue Lifecycle
|
||||
|
||||
The issue lifecycle is mainly driven by the core maintainers, but is good
|
||||
information for those contributing to Virtual Kubelet. All issue types
|
||||
follow the same general lifecycle. Differences are noted below.
|
||||
|
||||
1. Issue creation
|
||||
1. Triage
|
||||
- The maintainer in charge of triaging will apply the proper labels for the
|
||||
issue. This includes labels for priority, type, and metadata. If additional
|
||||
labels are needed in the future, we will add them.
|
||||
- If needed, clean up the title to succinctly and clearly state the issue.
|
||||
Also ensure that proposals are prefaced with "Proposal".
|
||||
- Add the issue to the correct milestone. If any questions come up, don't
|
||||
worry about adding the issue to a milestone until the questions are
|
||||
answered.
|
||||
- We attempt to do this process at least once per work day.
|
||||
1. Discussion
|
||||
- "Feature" and "Bug" issues should be connected to the PR that resolves it.
|
||||
- Whoever is working on a "Feature" or "Bug" issue (whether a maintainer or
|
||||
someone from the community), should either assign the issue to themself or
|
||||
make a comment in the issue saying that they are taking it.
|
||||
- "Proposal" and "Question" issues should remain open until they are
|
||||
either resolved or have remained inactive for more than 30 days. This will
|
||||
help keep the issue queue to a manageable size and reduce noise. Should the
|
||||
issue need to stay open, the `keep open` label can be added.
|
||||
1. Issue closure
|
||||
|
||||
## How to Contribute a Patch
|
||||
|
||||
1. If you haven't already done so, sign a Contributor License Agreement
|
||||
(see details above).
|
||||
2. Fork the repository, develop and test your code changes.
|
||||
3. Submit a pull request.
|
||||
|
||||
Your pull request will be reviewed according to the process defined in
|
||||
[reviewing.md](./reviewing.md).
|
||||
|
||||
## Code of conduct
|
||||
|
||||
This project has adopted the
|
||||
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the
|
||||
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any
|
||||
additional questions or comments.
|
||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM golang:alpine as builder
|
||||
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH /go
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates
|
||||
|
||||
COPY . /go/src/github.com/virtual-kubelet/virtual-kubelet
|
||||
|
||||
RUN set -x \
|
||||
&& apk add --no-cache --virtual .build-deps \
|
||||
git \
|
||||
gcc \
|
||||
libc-dev \
|
||||
libgcc \
|
||||
&& cd /go/src/github.com/virtual-kubelet/virtual-kubelet \
|
||||
&& CGO_ENABLED=0 go build -a -tags netgo -ldflags '-extldflags "-static"' -o /usr/bin/virtual-kubelet . \
|
||||
&& apk del .build-deps \
|
||||
&& rm -rf /go \
|
||||
&& echo "Build complete."
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /usr/bin/virtual-kubelet /usr/bin/virtual-kubelet
|
||||
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs
|
||||
|
||||
ENTRYPOINT [ "virtual-kubelet" ]
|
||||
CMD [ "--help" ]
|
||||
408
Gopkg.lock
generated
Normal file
408
Gopkg.lock
generated
Normal file
@@ -0,0 +1,408 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/go-autorest"
|
||||
packages = ["autorest/adal","autorest/date"]
|
||||
revision = "c67b24a8e30d876542a85022ebbdecf0e5a935e8"
|
||||
version = "v9.4.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Microsoft/go-winio"
|
||||
packages = ["."]
|
||||
revision = "78439966b38d69bf38227fbf57ac8a6fee70f69a"
|
||||
version = "v0.4.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
packages = ["."]
|
||||
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/PuerkitoBio/urlesc"
|
||||
packages = ["."]
|
||||
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
packages = ["."]
|
||||
revision = "a41693b7b7afb422c7ecb1028458ab27da047bbb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
|
||||
version = "v3.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dimchansky/utfbom"
|
||||
packages = ["."]
|
||||
revision = "6c6132ff69f0f6c088739067407b5d32c52e1d0f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/distribution"
|
||||
packages = ["digest","reference"]
|
||||
revision = "48294d928ced5dd9b378f7fd7c6f5da3ff3f2c89"
|
||||
version = "v2.6.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/engine-api"
|
||||
packages = ["types/strslice"]
|
||||
revision = "3d1601b9d2436a70b0dfc045a23f6503d19195df"
|
||||
version = "v0.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat","sockets","tlsconfig"]
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/emicklei/go-restful"
|
||||
packages = [".","log"]
|
||||
revision = "5741799b275a3c4a5a9623a993576d7545cf7b5c"
|
||||
version = "v2.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/emicklei/go-restful-swagger12"
|
||||
packages = ["."]
|
||||
revision = "dcef7f55730566d41eae5db10e7d6981829720f6"
|
||||
version = "1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/flynn/go-shlex"
|
||||
packages = ["."]
|
||||
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/ghodss/yaml"
|
||||
packages = ["."]
|
||||
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/go-openapi/jsonpointer"
|
||||
packages = ["."]
|
||||
revision = "779f45308c19820f1a69e9a4cd965f496e0da10f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/go-openapi/jsonreference"
|
||||
packages = ["."]
|
||||
revision = "36d33bfe519efae5632669801b180bf1a245da3b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/go-openapi/spec"
|
||||
packages = ["."]
|
||||
revision = "bfb48d37839bcacb52be3f539ffac5abcda7e195"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/go-openapi/swag"
|
||||
packages = ["."]
|
||||
revision = "cf0bdb963811675a4d7e74901cefc7411a1df939"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto","sortkeys"]
|
||||
revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02"
|
||||
version = "v0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/glog"
|
||||
packages = ["."]
|
||||
revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"]
|
||||
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/google/btree"
|
||||
packages = ["."]
|
||||
revision = "316fb6d3f031ae8f4d457c6c5186b9e3ded70435"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/google/gofuzz"
|
||||
packages = ["."]
|
||||
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
|
||||
version = "0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/googleapis/gnostic"
|
||||
packages = ["OpenAPIv2","compiler","extensions"]
|
||||
revision = "ee43cbb60db7bd22502942cccbc39059117352ab"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gregjones/httpcache"
|
||||
packages = [".","diskcache"]
|
||||
revision = "2bcd89a1743fd4b373f7370ce8ddc14dfbd18229"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
|
||||
revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/hyperhq/hyper-api"
|
||||
packages = ["client","client/transport","client/transport/cancellable","signature","types","types/blkiodev","types/container","types/filters","types/network","types/reference","types/registry","types/strslice","types/time","types/versions"]
|
||||
revision = "18c77d3f9fe0abebb41b45c12f383ecac46f4ff1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/hyperhq/hypercli"
|
||||
packages = ["pkg/urlutil"]
|
||||
revision = "860cca29de31268664bf04bd7a87c3ca2c1d675e"
|
||||
version = "v1.10.16"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/hyperhq/libcompose"
|
||||
packages = ["config","utils","yaml"]
|
||||
revision = "15d3a105140f968f5d4f62d2f44afd22a24a98fb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/imdario/mergo"
|
||||
packages = ["."]
|
||||
revision = "7fe0c75c13abdee74b09fcacef5ea1c6bba6a874"
|
||||
version = "0.2.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/json-iterator/go"
|
||||
packages = ["."]
|
||||
revision = "f7279a603edee96fe7764d3de9c6ff8cf9970994"
|
||||
version = "1.0.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/ratelimit"
|
||||
packages = ["."]
|
||||
revision = "59fac5042749a5afb9af70e813da1dd5474f0167"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
||||
version = "v1.7.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mailru/easyjson"
|
||||
packages = ["buffer","jlexer","jwriter"]
|
||||
revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "06020f85339e21b2478f756a78e295255ffa4d6a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
revision = "16398bac157da96aa88f98a2df640c7f32af1da2"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/petar/GoLLRB"
|
||||
packages = ["llrb"]
|
||||
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/peterbourgon/diskv"
|
||||
packages = ["."]
|
||||
revision = "5f041e8faa004a95c88a202771f4cc3e991971e6"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [".","mem"]
|
||||
revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
|
||||
version = "v0.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/xeipuuv/gojsonpointer"
|
||||
packages = ["."]
|
||||
revision = "6fe8760cad3569743d51ddbb243b26f8456742dc"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/xeipuuv/gojsonreference"
|
||||
packages = ["."]
|
||||
revision = "e02fc20de94c78484cd5ffb007f8af96be030a45"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/xeipuuv/gojsonschema"
|
||||
packages = ["."]
|
||||
revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","http2","http2/hpack","idna","lex/httplex","proxy"]
|
||||
revision = "6921abc35dffd00438a0c020584ce560108737ea"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "b76f9891dc1d975623261def70f9b89661f5baab"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
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"]
|
||||
revision = "572a2b141f625f4360cf42a41a43622067e0510b"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
|
||||
version = "v0.9.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "k8s.io/api"
|
||||
packages = ["admissionregistration/v1alpha1","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","extensions/v1beta1","networking/v1","policy/v1beta1","rbac/v1","rbac/v1alpha1","rbac/v1beta1","scheduling/v1alpha1","settings/v1alpha1","storage/v1","storage/v1beta1"]
|
||||
revision = "218912509d74a117d05a718bb926d0948e531c20"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "k8s.io/apimachinery"
|
||||
packages = ["pkg/api/equality","pkg/api/errors","pkg/api/meta","pkg/api/resource","pkg/apis/meta/v1","pkg/apis/meta/v1/unstructured","pkg/apis/meta/v1alpha1","pkg/conversion","pkg/conversion/queryparams","pkg/conversion/unstructured","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/clock","pkg/util/diff","pkg/util/errors","pkg/util/framer","pkg/util/intstr","pkg/util/json","pkg/util/net","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/reflect"]
|
||||
revision = "18a564baac720819100827c16fdebcadb05b2d0d"
|
||||
|
||||
[[projects]]
|
||||
name = "k8s.io/client-go"
|
||||
packages = ["discovery","kubernetes","kubernetes/scheme","kubernetes/typed/admissionregistration/v1alpha1","kubernetes/typed/apps/v1beta1","kubernetes/typed/apps/v1beta2","kubernetes/typed/authentication/v1","kubernetes/typed/authentication/v1beta1","kubernetes/typed/authorization/v1","kubernetes/typed/authorization/v1beta1","kubernetes/typed/autoscaling/v1","kubernetes/typed/autoscaling/v2beta1","kubernetes/typed/batch/v1","kubernetes/typed/batch/v1beta1","kubernetes/typed/batch/v2alpha1","kubernetes/typed/certificates/v1beta1","kubernetes/typed/core/v1","kubernetes/typed/extensions/v1beta1","kubernetes/typed/networking/v1","kubernetes/typed/policy/v1beta1","kubernetes/typed/rbac/v1","kubernetes/typed/rbac/v1alpha1","kubernetes/typed/rbac/v1beta1","kubernetes/typed/scheduling/v1alpha1","kubernetes/typed/settings/v1alpha1","kubernetes/typed/storage/v1","kubernetes/typed/storage/v1beta1","pkg/version","rest","rest/watch","tools/auth","tools/clientcmd","tools/clientcmd/api","tools/clientcmd/api/latest","tools/clientcmd/api/v1","tools/metrics","tools/reference","transport","util/cert","util/flowcontrol","util/homedir","util/integer"]
|
||||
revision = "2ae454230481a7cb5544325e12ad7658ecccd19b"
|
||||
version = "v5.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = ["pkg/common"]
|
||||
revision = "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "e476a3cf3fc7c556db93b5f202d0f578cdca9cda551563756ea30261aff2bd9b"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
50
Gopkg.toml
Normal file
50
Gopkg.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
version = "0.0.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "0.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/hyperhq/hyper-api"
|
||||
revision = "18c77d3f9fe0abebb41b45c12f383ecac46f4ff1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/hyperhq/libcompose"
|
||||
revision = "15d3a105140f968f5d4f62d2f44afd22a24a98fb"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/xeipuuv/gojsonschema"
|
||||
revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
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
|
||||
182
Makefile
Normal file
182
Makefile
Normal file
@@ -0,0 +1,182 @@
|
||||
IMPORT_PATH := github.com/virtual-kubelet/virtual-kubelet
|
||||
DOCKER_IMAGE := virtual-kubelet
|
||||
build_dir := $(CURDIR)/build
|
||||
dist_dir := $(CURDIR)/dist
|
||||
exec := $(DOCKER_IMAGE)
|
||||
github_repo := virtual-kubelet/virtual-kubelet
|
||||
binary := virtual-kubelet
|
||||
|
||||
# comment this line out for quieter things
|
||||
V := 1 # When V is set, print commands and build progress.
|
||||
|
||||
# Space separated patterns of packages to skip in list, test, format.
|
||||
IGNORED_PACKAGES := /vendor/
|
||||
|
||||
.PHONY: all
|
||||
all: test build
|
||||
|
||||
.PHONY: safebuild
|
||||
# safebuild builds inside a docker container with no clingons from your $GOPATH
|
||||
safebuild:
|
||||
@echo "Building..."
|
||||
$Q docker build -t $(DOCKER_IMAGE):$(VERSION) .
|
||||
|
||||
.PHONY: build
|
||||
build: authors
|
||||
@echo "Building..."
|
||||
$Q go build -o bin/$(binary) $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH)
|
||||
|
||||
.PHONY: tags
|
||||
tags:
|
||||
@echo "Listing tags..."
|
||||
$Q @git tag
|
||||
|
||||
.PHONY: release
|
||||
release: clean-dist build $(GOPATH)/bin/goreleaser
|
||||
goreleaser
|
||||
|
||||
|
||||
### Code not in the repository root? Another binary? Add to the path like this.
|
||||
# .PHONY: otherbin
|
||||
# otherbin: .GOPATH/.ok
|
||||
# $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/otherbin
|
||||
|
||||
##### ^^^^^^ EDIT ABOVE ^^^^^^ #####
|
||||
|
||||
##### =====> Utility targets <===== #####
|
||||
|
||||
.PHONY: clean test list cover format docker deps
|
||||
|
||||
deps: setup
|
||||
@echo "Ensuring Dependencies..."
|
||||
$Q go env
|
||||
$Q dep ensure
|
||||
|
||||
docker:
|
||||
@echo "Docker Build..."
|
||||
$Q docker build -t $(DOCKER_IMAGE) .
|
||||
|
||||
clean: clean-dist clean-build
|
||||
@echo "Clean..."
|
||||
$Q rm -rf bin
|
||||
|
||||
.PHONY: clean-build
|
||||
clean-build:
|
||||
@echo "Removing cross-compilation files"
|
||||
rm -rf $(build_dir)
|
||||
|
||||
.PHONY: clean-dist
|
||||
clean-dist:
|
||||
@echo "Removing distribution files"
|
||||
rm -rf $(dist_dir)
|
||||
|
||||
test:
|
||||
@echo "Testing..."
|
||||
$Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run
|
||||
ifndef CI
|
||||
@echo "Testing Outside CI..."
|
||||
$Q go vet $(allpackages)
|
||||
$Q GODEBUG=cgocheck=2 go test -race $(allpackages)
|
||||
else
|
||||
@echo "Testing in CI..."
|
||||
$Q mkdir -p test
|
||||
$Q ( go vet $(allpackages); echo $$? ) | \
|
||||
tee test/vet.txt | sed '$$ d'; exit $$(tail -1 test/vet.txt)
|
||||
$Q ( GODEBUG=cgocheck=2 go test -v -race $(allpackages); echo $$? ) | \
|
||||
tee test/output.txt | sed '$$ d'; exit $$(tail -1 test/output.txt)
|
||||
endif
|
||||
|
||||
list:
|
||||
@echo "List..."
|
||||
@echo $(allpackages)
|
||||
|
||||
cover: $(GOPATH)/bin/gocovmerge
|
||||
@echo "Coverage Report..."
|
||||
@echo "NOTE: make cover does not exit 1 on failure, don't use it to check for tests success!"
|
||||
$Q rm -f .GOPATH/cover/*.out cover/all.merged
|
||||
$(if $V,@echo "-- go test -coverpkg=./... -coverprofile=cover/... ./...")
|
||||
@for MOD in $(allpackages); do \
|
||||
go test -coverpkg=`echo $(allpackages)|tr " " ","` \
|
||||
-coverprofile=cover/unit-`echo $$MOD|tr "/" "_"`.out \
|
||||
$$MOD 2>&1 | grep -v "no packages being tested depend on"; \
|
||||
done
|
||||
$Q gocovmerge cover/*.out > cover/all.merged
|
||||
ifndef CI
|
||||
@echo "Coverage Report..."
|
||||
$Q go tool cover -html .GOPATH/cover/all.merged
|
||||
else
|
||||
@echo "Coverage Report In CI..."
|
||||
$Q go tool cover -html .GOPATH/cover/all.merged -o .GOPATH/cover/all.html
|
||||
endif
|
||||
@echo ""
|
||||
@echo "=====> Total test coverage: <====="
|
||||
@echo ""
|
||||
$Q go tool cover -func .GOPATH/cover/all.merged
|
||||
|
||||
format: $(GOPATH)/bin/goimports
|
||||
@echo "Formatting..."
|
||||
$Q find . -iname \*.go | grep -v \
|
||||
-e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) | xargs goimports -w
|
||||
|
||||
##### =====> Internals <===== #####
|
||||
|
||||
.PHONY: setup
|
||||
setup: clean
|
||||
@echo "Setup..."
|
||||
if ! grep "/bin" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/bin" >> .gitignore; \
|
||||
fi
|
||||
if ! grep "/dist" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/dist" >> .gitignore; \
|
||||
fi
|
||||
if ! grep "/build" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/build" >> .gitignore; \
|
||||
fi
|
||||
if ! grep "/cover" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/cover" >> .gitignore; \
|
||||
fi
|
||||
if ! grep "/bin" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/bin" >> .gitignore; \
|
||||
fi
|
||||
if ! grep "/test" .gitignore > /dev/null 2>&1; then \
|
||||
echo "/test" >> .gitignore; \
|
||||
fi
|
||||
mkdir -p cover
|
||||
mkdir -p bin
|
||||
mkdir -p test
|
||||
go get -u github.com/golang/dep/cmd/dep
|
||||
go get github.com/wadey/gocovmerge
|
||||
go get golang.org/x/tools/cmd/goimports
|
||||
go get github.com/mitchellh/gox
|
||||
go get github.com/goreleaser/goreleaser
|
||||
|
||||
VERSION := $(shell git describe --tags --always --dirty="-dev")
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H:%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "github.com/virtual-kubelet/virtual-kubelet/version.Version=$(VERSION)" -X "github.com/virtual-kubelet/virtual-kubelet/version.BuildTime=$(DATE)"'
|
||||
|
||||
# assuming go 1.9 here!!
|
||||
_allpackages = $(shell go list ./...)
|
||||
|
||||
# memoize allpackages, so that it's executed only once and only if used
|
||||
allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages)
|
||||
|
||||
|
||||
Q := $(if $V,,@)
|
||||
|
||||
|
||||
$(GOPATH)/bin/gocovmerge:
|
||||
@echo "Checking Coverage Tool Installation..."
|
||||
@test -d $(GOPATH)/src/github.com/wadey/gocovmerge || \
|
||||
{ echo "Vendored gocovmerge not found, try running 'make setup'..."; exit 1; }
|
||||
$Q go install github.com/wadey/gocovmerge
|
||||
$(GOPATH)/bin/goimports:
|
||||
@echo "Checking Import Tool Installation..."
|
||||
@test -d $(GOPATH)/src/golang.org/x/tools/cmd/goimports || \
|
||||
{ echo "Vendored goimports not found, try running 'make setup'..."; exit 1; }
|
||||
$Q go install golang.org/x/tools/cmd/goimports
|
||||
|
||||
$(GOPATH)/bin/goreleaser:
|
||||
go get -u github.com/goreleaser/goreleaser
|
||||
|
||||
authors:
|
||||
git log --all --format='%aN <%cE>' | sort -u | sed -n '/github/!p' > AUTHORS
|
||||
141
README.md
Normal file
141
README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Virtual Kubelet
|
||||
|
||||
Virtual Kubelet is an open source [Kubernetes kubelet](https://kubernetes.io/docs/reference/generated/kubelet/) implementation that masquerades as a kubelet for the purposes of connecting Kubernetes to other APIs. This allows the nodes to be backed by other services like ACI, Hyper.sh, AWS, etc. This connector features a pluggable architecture and direct use of Kubernetes primitives, making it much easier to build on.
|
||||
|
||||
We invite the Kubernetes ecosystem to join us in empowering developers to build
|
||||
upon our base.
|
||||
|
||||
Please note this software is experimental and should not be used for anything
|
||||
resembling a production workload.
|
||||
|
||||
The best description is "Kubernetes API on top, programmable back."
|
||||
|
||||
#### Table of Contents
|
||||
|
||||
* [How It Works](#how-it-works)
|
||||
* [Usage](#usage)
|
||||
* [Providers](#providers)
|
||||
+ [Azure Container Instances Provider](#azure-container-instances-provider)
|
||||
+ [Hyper.sh Provider](#hypersh-provider)
|
||||
+ [Adding a New Provider via the Provider Interface](#adding-a-new-provider-via-the-provider-interface)
|
||||
* [Testing](#testing)
|
||||
+ [Testing the Azure Provider Client](#testing-the-azure-provider-client)
|
||||
* [Contributing](#contributing)
|
||||
|
||||
## How It Works
|
||||
|
||||
The diagram below illustrates how Virtual-Kubelet works.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Deploy a Kubernetes cluster and make sure it's reachable.
|
||||
|
||||
Run the binary with your chosen provider:
|
||||
|
||||
```bash
|
||||
./bin/virtual-kubelet --provider <your provider>
|
||||
```
|
||||
|
||||
Now that the virtual-kubelet is deployed run `kubectl get nodes` and you should see
|
||||
a `virtual-kubelet` node.
|
||||
|
||||
## Providers
|
||||
|
||||
This project features a pluggable provider interface developers can implement
|
||||
that defines the actions of a typical kubelet.
|
||||
|
||||
This enables on-demand and nearly instantaneous container compute, orchestrated
|
||||
by Kubernetes, without having VM infrastructure to manage and while still
|
||||
leveraging the portable Kubernetes API.
|
||||
|
||||
### Azure Container Instances Provider
|
||||
|
||||
The Azure Container Instances Provider allows you to utilize both
|
||||
typical pods on VMs and Azure Container instances simultaneously in the
|
||||
same Kubernetes cluster.
|
||||
|
||||
```bash
|
||||
./bin/virtual-kubelet --provider azure
|
||||
```
|
||||
|
||||
### Hyper.sh Provider
|
||||
|
||||
The Hyper.sh Provider allows Kubernetes clusters to deploy Hyper.sh containers
|
||||
and manage both typical pods on VMs and Hyper.sh containers in the same
|
||||
Kubernetes cluster.
|
||||
|
||||
```bash
|
||||
./bin/virtual-kubelet --provider hyper
|
||||
```
|
||||
|
||||
### Adding a New Provider via the Provider Interface
|
||||
|
||||
The structure we chose allows you to have all the power of the Kubernetes API
|
||||
on top with a pluggable interface.
|
||||
|
||||
Create a new directory for your provider under `providers` and implement the
|
||||
following interface. Then add your new provider under the others in the
|
||||
[`virtual-kubelet/provider.go`](virtual-kubelet/provider.go) file.
|
||||
|
||||
```go
|
||||
// Provider contains the methods required to implement a virtual-kubelet provider.
|
||||
type Provider interface {
|
||||
// CreatePod takes a Kubernetes Pod and deploys it within the provider.
|
||||
CreatePod(pod *v1.Pod) error
|
||||
|
||||
// UpdatePod takes a Kubernetes Pod and updates it within the provider.
|
||||
UpdatePod(pod *v1.Pod) error
|
||||
|
||||
// DeletePod takes a Kubernetes Pod and deletes it from the provider.
|
||||
DeletePod(pod *v1.Pod) error
|
||||
|
||||
// GetPod retrieves a pod by name from the provider (can be cached).
|
||||
GetPod(name string) (*v1.Pod, error)
|
||||
|
||||
// GetPodStatus retrievesthe status of a pod by name from the provider.
|
||||
GetPodStatus(name string) (*v1.PodStatus, error)
|
||||
|
||||
// GetPods retrieves a list of all pods running on the provider (can be cached).
|
||||
GetPods() ([]*v1.Pod, error)
|
||||
|
||||
// Capacity returns a resource list with the capacity constraints of the provider.
|
||||
Capacity() v1.ResourceList
|
||||
|
||||
// NodeConditions returns a list of conditions (Ready, OutOfDisk, etc), which is polled periodically to update the node status
|
||||
// within Kuberentes.
|
||||
NodeConditions() []v1.NodeCondition
|
||||
|
||||
// OperatingSystem returns the operating system the provider is for.
|
||||
OperatingSystem() string
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Running the unit tests locally is as simple as `make test`.
|
||||
|
||||
### Testing the Azure Provider Client
|
||||
|
||||
The unit tests for the [`azure`](providers/azure/) provider require a `credentials.json`
|
||||
file exist in the root of this directory or that you have `AZURE_AUTH_LOCATION`
|
||||
set to a credentials file.
|
||||
|
||||
You can generate this file by following the instructions listed in the
|
||||
[README](providers/azure/client/README.md) for that package.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
BIN
charts/virtual-kubelet.tgz
Normal file
BIN
charts/virtual-kubelet.tgz
Normal file
Binary file not shown.
8
charts/virtual-kubelet/Chart.yaml
Normal file
8
charts/virtual-kubelet/Chart.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
name: virtual-kubelet
|
||||
version: 0.1.0
|
||||
description: a Helm chart to install virtual kubelet inside a Kubernetes cluster.
|
||||
sources:
|
||||
- https://github.com/virtual-kubelet/virtual-kubelet
|
||||
maintainers:
|
||||
- name: Robbie Zhang
|
||||
email: junjiez@microsoft.com
|
||||
21
charts/virtual-kubelet/templates/NOTES.txt
Normal file
21
charts/virtual-kubelet/templates/NOTES.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- if and .Values.env.azureClientId .Values.env.azureClientKey .Values.env.azureTenantId .Values.env.azureSubscriptionId .Values.env.aciResourceGroup -}}
|
||||
|
||||
The virtual kubelet is getting deployed on your cluster.
|
||||
|
||||
To verify that virtual kubelet has started, run:
|
||||
|
||||
kubectl --namespace={{ .Release.Namespace }} get pods -l "app={{ template "fullname" . }}"
|
||||
|
||||
{{- else -}}
|
||||
##############################################################################
|
||||
#### ERROR: You are missing required values in the values.yaml file. ####
|
||||
##############################################################################
|
||||
|
||||
This deployment will be incomplete until all the required fields in the values.yaml file have been provided.
|
||||
|
||||
To update, run:
|
||||
|
||||
helm upgrade {{ .Release.Name }} \
|
||||
--set env.azureClientId=<YOUR-AZURECLIENTID-HERE>,env.azureClientKey=<YOUR-AZURECLIENTKEY-HERE>,env.azureTenantId=<YOUR-AZURETENANTID-HERE>,env.azureSubscriptionId=<YOUR-AZURESUBSCRIPTIONID-HERE>,env.aciResourceGroup=<YOUR-ACIRESOURCEGROUP-HERE>
|
||||
|
||||
{{- end }}
|
||||
16
charts/virtual-kubelet/templates/_helpers.tpl
Normal file
16
charts/virtual-kubelet/templates/_helpers.tpl
Normal file
@@ -0,0 +1,16 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 24 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "fullname" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
32
charts/virtual-kubelet/templates/deployment.yaml
Normal file
32
charts/virtual-kubelet/templates/deployment.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "fullname" . }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ template "fullname" . }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: AZURE_AUTH_LOCATION
|
||||
value: /etc/virtual-kubelet/credentials.json
|
||||
- name: ACI_RESOURCE_GROUP
|
||||
value: {{ .Values.env.aciResourceGroup }}
|
||||
- name: ACI_REGION
|
||||
value: {{ default "westus" .Values.env.aciRegion }}
|
||||
volumeMounts:
|
||||
- name: credentials
|
||||
mountPath: "/etc/virtual-kubelet"
|
||||
readOnly: true
|
||||
command: ["virtual-kubelet/"]
|
||||
args: ["--provider", "azure"]
|
||||
volumes:
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: {{ template "fullname" . }}
|
||||
7
charts/virtual-kubelet/templates/secrets.yaml
Normal file
7
charts/virtual-kubelet/templates/secrets.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "fullname" . }}
|
||||
type: Opaque
|
||||
data:
|
||||
credentials.json: {{ printf "{ \"clientId\": \"%s\", \"clientSecret\": \"%s\", \"subscriptionId\": \"%s\", \"tenantId\": \"%s\", \"activeDirectoryEndpointUrl\": \"https://login.microsoftonline.com/\", \"resourceManagerEndpointUrl\": \"https://management.azure.com/\", \"activeDirectoryGraphResourceId\": \"https://graph.windows.net/\", \"sqlManagementEndpointUrl\": \"database.windows.net\", \"galleryEndpointUrl\": \"https://gallery.azure.com/\", \"managementEndpointUrl\": \"https://management.core.windows.net/\" }" (default "MISSING" .Values.env.azureClientId) (default "MISSING" .Values.env.azureClientKey) (default "MISSING" .Values.env.azureSubscriptionId) (default "MISSING" .Values.env.azureTenantId) | b64enc | quote }}
|
||||
11
charts/virtual-kubelet/values.yaml
Normal file
11
charts/virtual-kubelet/values.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
image:
|
||||
repository: virtual-kubelet/virtual-kubelet
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
env:
|
||||
azureClientId:
|
||||
azureClientKey:
|
||||
azureTenantId:
|
||||
azureSubscriptionId:
|
||||
aciResourceGroup:
|
||||
aciRegion:
|
||||
134
cmd/root.go
Normal file
134
cmd/root.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright © 2017 The virtual-kubelet authors
|
||||
//
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers"
|
||||
vkubelet "github.com/virtual-kubelet/virtual-kubelet/vkubelet"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
var kubeletConfig string
|
||||
var kubeConfig string
|
||||
var kubeNamespace string
|
||||
var nodeName string
|
||||
var operatingSystem string
|
||||
var provider string
|
||||
var providerConfig string
|
||||
var taint string
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "virtual-kubelet",
|
||||
Short: "A brief description of your application",
|
||||
Long: `A longer description that spans multiple lines and likely contains
|
||||
examples and usage of using your application. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
f, err := vkubelet.New(nodeName, operatingSystem, kubeNamespace, kubeConfig, taint, provider, providerConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
f.Run()
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
//RootCmd.PersistentFlags().StringVar(&kubeletConfig, "config", "", "config file (default is $HOME/.virtual-kubelet.yaml)")
|
||||
RootCmd.PersistentFlags().StringVar(&kubeConfig, "kubeconfig", "", "config file (default is $HOME/.kube/config)")
|
||||
RootCmd.PersistentFlags().StringVar(&kubeNamespace, "namespace", "", "kuberentes namespace (default is 'all')")
|
||||
RootCmd.PersistentFlags().StringVar(&nodeName, "nodename", "virtual-kubelet", "kubernetes node name")
|
||||
RootCmd.PersistentFlags().StringVar(&operatingSystem, "os", "Linux", "Operating System (Linux/Windows)")
|
||||
RootCmd.PersistentFlags().StringVar(&provider, "provider", "", "cloud provider")
|
||||
RootCmd.PersistentFlags().StringVar(&taint, "taint", "", "apply taint to node, making scheduling explicit")
|
||||
RootCmd.PersistentFlags().StringVar(&providerConfig, "provider-config", "", "cloud provider configuration file")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if provider == "" {
|
||||
fmt.Println("You must supply a cloud provider option: use --provider")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Find home directory.
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if kubeletConfig != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(kubeletConfig)
|
||||
} else {
|
||||
// Search config in home directory with name ".virtual-kubelet" (without extension).
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigName(".virtual-kubelet")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
if kubeConfig == "" {
|
||||
kubeConfig = filepath.Join(home, ".kube", "config")
|
||||
|
||||
}
|
||||
|
||||
if kubeNamespace == "" {
|
||||
kubeNamespace = corev1.NamespaceAll
|
||||
}
|
||||
|
||||
// Validate operating system.
|
||||
ok, _ := providers.ValidOperatingSystems[operatingSystem]
|
||||
if !ok {
|
||||
fmt.Printf("Operating system '%s' not supported. Valid options are %s\n", operatingSystem, strings.Join(providers.ValidOperatingSystems.Names(), " | "))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
51
cmd/version.go
Normal file
51
cmd/version.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2017 NAME HERE <EMAIL ADDRESS>
|
||||
//
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "A brief description of your command",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Version: %s, Built: %s", version.Version, version.BuildTime)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// versionCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
4
diagram.svg
Normal file
4
diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 236 KiB |
15
examples/nginx-pod.yaml
Normal file
15
examples/nginx-pod.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
imagePullPolicy: Always
|
||||
name: nginx
|
||||
resources:
|
||||
requests:
|
||||
memory: 1G
|
||||
cpu: 1
|
||||
dnsPolicy: ClusterFirst
|
||||
nodeName: virtual-kubelet
|
||||
21
main.go
Normal file
21
main.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright © 2017 The virtual-kubelet authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/virtual-kubelet/virtual-kubelet/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
295
manager/resource.go
Normal file
295
manager/resource.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ResourceManager works a cache for pods assigned to this virtual node within Kubernetes.
|
||||
// New ResourceManagers should be created with the NewResourceManager() function.
|
||||
type ResourceManager struct {
|
||||
sync.RWMutex
|
||||
k8sClient *kubernetes.Clientset
|
||||
|
||||
pods map[string]*v1.Pod
|
||||
configMapRef map[string]int64
|
||||
configMaps map[string]*v1.ConfigMap
|
||||
secretRef map[string]int64
|
||||
secrets map[string]*v1.Secret
|
||||
}
|
||||
|
||||
// NewResourceManager returns a ResourceManager with the internal maps initialized.
|
||||
func NewResourceManager(k8sClient *kubernetes.Clientset) *ResourceManager {
|
||||
rm := ResourceManager{
|
||||
pods: make(map[string]*v1.Pod, 0),
|
||||
configMapRef: make(map[string]int64, 0),
|
||||
secretRef: make(map[string]int64, 0),
|
||||
configMaps: make(map[string]*v1.ConfigMap, 0),
|
||||
secrets: make(map[string]*v1.Secret, 0),
|
||||
k8sClient: k8sClient,
|
||||
}
|
||||
|
||||
go rm.watchConfigMaps()
|
||||
go rm.watchSecrets()
|
||||
|
||||
tick := time.Tick(5 * time.Minute)
|
||||
go func() {
|
||||
for range tick {
|
||||
rm.Lock()
|
||||
for n, c := range rm.secretRef {
|
||||
if c <= 0 {
|
||||
delete(rm.secrets, n)
|
||||
}
|
||||
}
|
||||
for n := range rm.secrets {
|
||||
if _, ok := rm.secretRef[n]; !ok {
|
||||
delete(rm.secrets, n)
|
||||
}
|
||||
}
|
||||
for n, c := range rm.configMapRef {
|
||||
if c <= 0 {
|
||||
delete(rm.configMaps, n)
|
||||
}
|
||||
}
|
||||
for n := range rm.configMaps {
|
||||
if _, ok := rm.configMapRef[n]; !ok {
|
||||
delete(rm.configMaps, n)
|
||||
}
|
||||
}
|
||||
rm.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
return &rm
|
||||
}
|
||||
|
||||
// SetPods clears the internal cache and populates it with the supplied pods.
|
||||
func (rm *ResourceManager) SetPods(pods *v1.PodList) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
rm.pods = make(map[string]*v1.Pod, len(pods.Items))
|
||||
rm.configMapRef = make(map[string]int64, 0)
|
||||
rm.secretRef = make(map[string]int64, 0)
|
||||
rm.configMaps = make(map[string]*v1.ConfigMap, len(pods.Items))
|
||||
rm.secrets = make(map[string]*v1.Secret, len(pods.Items))
|
||||
|
||||
for _, p := range pods.Items {
|
||||
if p.Status.Phase == v1.PodSucceeded {
|
||||
continue
|
||||
}
|
||||
rm.pods[p.Name] = &p
|
||||
|
||||
rm.incrementRefCounters(&p)
|
||||
}
|
||||
}
|
||||
|
||||
// AddPod adds a pod to the internal cache.
|
||||
func (rm *ResourceManager) AddPod(p *v1.Pod) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
if p.Status.Phase == v1.PodSucceeded {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := rm.pods[p.Name]; ok {
|
||||
rm.UpdatePod(p)
|
||||
return
|
||||
}
|
||||
|
||||
rm.pods[p.Name] = p
|
||||
rm.incrementRefCounters(p)
|
||||
}
|
||||
|
||||
// UpdatePod updates the supplied pod in the cache.
|
||||
func (rm *ResourceManager) UpdatePod(p *v1.Pod) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
if p.Status.Phase == v1.PodSucceeded {
|
||||
delete(rm.pods, p.Name)
|
||||
}
|
||||
|
||||
if old, ok := rm.pods[p.Name]; ok {
|
||||
rm.decrementRefCounters(old)
|
||||
}
|
||||
rm.incrementRefCounters(p)
|
||||
|
||||
rm.pods[p.Name] = p
|
||||
}
|
||||
|
||||
// DeletePod removes the pod from the cache.
|
||||
func (rm *ResourceManager) DeletePod(p *v1.Pod) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
if old, ok := rm.pods[p.Name]; ok {
|
||||
rm.decrementRefCounters(old)
|
||||
delete(rm.pods, p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPod retrieves the specified pod from the cache. It returns nil if a pod is not found.
|
||||
func (rm *ResourceManager) GetPod(name string) *v1.Pod {
|
||||
rm.RLock()
|
||||
defer rm.RUnlock()
|
||||
|
||||
if p, ok := rm.pods[name]; ok {
|
||||
return p
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPods returns a list of all known pods assigned to this virtual node.
|
||||
func (rm *ResourceManager) GetPods() []*v1.Pod {
|
||||
rm.RLock()
|
||||
defer rm.RUnlock()
|
||||
|
||||
pods := make([]*v1.Pod, 0, len(rm.pods))
|
||||
for _, p := range rm.pods {
|
||||
pods = append(pods, p)
|
||||
}
|
||||
|
||||
return pods
|
||||
}
|
||||
|
||||
// GetConfigMap returns the specified ConfigMap from Kubernetes. It retrieves it from cache if there
|
||||
func (rm *ResourceManager) GetConfigMap(name, namespace string) (*v1.ConfigMap, error) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
if cm, ok := rm.configMaps[name]; ok {
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
var opts metav1.GetOptions
|
||||
cm, err := rm.k8sClient.CoreV1().ConfigMaps(namespace).Get(name, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rm.configMaps[name] = cm
|
||||
|
||||
return cm, err
|
||||
}
|
||||
|
||||
// GetSecret returns the specified ConfigMap from Kubernetes. It retrieves it from cache if there
|
||||
func (rm *ResourceManager) GetSecret(name, namespace string) (*v1.Secret, error) {
|
||||
rm.Lock()
|
||||
defer rm.Unlock()
|
||||
|
||||
if secret, ok := rm.secrets[name]; ok {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
var opts metav1.GetOptions
|
||||
secret, err := rm.k8sClient.CoreV1().Secrets(namespace).Get(name, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rm.secrets[name] = secret
|
||||
|
||||
return secret, err
|
||||
}
|
||||
|
||||
// watchConfigMaps monitors the kubernetes API for modifications and deletions of configmaps
|
||||
// it evicts them from the internal cache
|
||||
func (rm *ResourceManager) watchConfigMaps() {
|
||||
var opts metav1.ListOptions
|
||||
w, err := rm.k8sClient.CoreV1().ConfigMaps(v1.NamespaceAll).Watch(opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-w.ResultChan():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rm.Lock()
|
||||
switch ev.Type {
|
||||
case watch.Modified:
|
||||
delete(rm.configMaps, ev.Object.(*v1.ConfigMap).Name)
|
||||
case watch.Deleted:
|
||||
delete(rm.configMaps, ev.Object.(*v1.ConfigMap).Name)
|
||||
}
|
||||
rm.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchSecretes monitors the kubernetes API for modifications and deletions of secrets
|
||||
// it evicts them from the internal cache
|
||||
func (rm *ResourceManager) watchSecrets() {
|
||||
var opts metav1.ListOptions
|
||||
w, err := rm.k8sClient.CoreV1().ConfigMaps(v1.NamespaceAll).Watch(opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-w.ResultChan():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rm.Lock()
|
||||
switch ev.Type {
|
||||
case watch.Modified:
|
||||
delete(rm.configMaps, ev.Object.(*v1.ConfigMap).Name)
|
||||
case watch.Deleted:
|
||||
delete(rm.configMaps, ev.Object.(*v1.ConfigMap).Name)
|
||||
}
|
||||
rm.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *ResourceManager) incrementRefCounters(p *v1.Pod) {
|
||||
for _, c := range p.Spec.Containers {
|
||||
for _, e := range c.Env {
|
||||
if e.ValueFrom.ConfigMapKeyRef != nil {
|
||||
rm.configMapRef[e.ValueFrom.ConfigMapKeyRef.Name]++
|
||||
}
|
||||
|
||||
if e.ValueFrom.SecretKeyRef != nil {
|
||||
rm.secretRef[e.ValueFrom.SecretKeyRef.Name]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range p.Spec.Volumes {
|
||||
if v.VolumeSource.Secret != nil {
|
||||
rm.secretRef[v.VolumeSource.Secret.SecretName]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *ResourceManager) decrementRefCounters(p *v1.Pod) {
|
||||
for _, c := range p.Spec.Containers {
|
||||
for _, e := range c.Env {
|
||||
if e.ValueFrom.ConfigMapKeyRef != nil {
|
||||
rm.configMapRef[e.ValueFrom.ConfigMapKeyRef.Name]--
|
||||
}
|
||||
|
||||
if e.ValueFrom.SecretKeyRef != nil {
|
||||
rm.secretRef[e.ValueFrom.SecretKeyRef.Name]--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range p.Spec.Volumes {
|
||||
if v.VolumeSource.Secret != nil {
|
||||
rm.secretRef[v.VolumeSource.Secret.SecretName]--
|
||||
}
|
||||
}
|
||||
}
|
||||
104
manager/resource_test.go
Normal file
104
manager/resource_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeClient *kubernetes.Clientset
|
||||
)
|
||||
|
||||
func init() {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
// if you want to change the loading rules (which files in which order), you can do so here
|
||||
|
||||
configOverrides := &clientcmd.ConfigOverrides{}
|
||||
// if you want to change override values or bind them to flags, there are methods to help you
|
||||
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
|
||||
config, err := kubeConfig.ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal("unable to create client config")
|
||||
}
|
||||
fakeClient, err = kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
log.Fatal("unable to create new clientset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceManager(t *testing.T) {
|
||||
pm := NewResourceManager(fakeClient)
|
||||
pod1Name := "Pod1"
|
||||
pod1 := makePod(pod1Name)
|
||||
pm.AddPod(pod1)
|
||||
|
||||
pods := pm.GetPods()
|
||||
if len(pods) != 1 {
|
||||
t.Errorf("Got %d, expected 1 pod", len(pods))
|
||||
}
|
||||
gotPod1 := pm.GetPod(pod1Name)
|
||||
if gotPod1.Name != pod1.Name {
|
||||
t.Errorf("Got %s, wanted %s", gotPod1.Name, pod1.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceManagerDeletePod(t *testing.T) {
|
||||
pm := NewResourceManager(fakeClient)
|
||||
pod1Name := "Pod1"
|
||||
pod1 := makePod(pod1Name)
|
||||
pm.AddPod(pod1)
|
||||
pods := pm.GetPods()
|
||||
if len(pods) != 1 {
|
||||
t.Errorf("Got %d, expected 1 pod", len(pods))
|
||||
}
|
||||
pm.DeletePod(pod1)
|
||||
|
||||
pods = pm.GetPods()
|
||||
if len(pods) != 0 {
|
||||
t.Errorf("Got %d, expected 0 pods", len(pods))
|
||||
}
|
||||
}
|
||||
func makePod(name string) *v1.Pod {
|
||||
pod := &v1.Pod{}
|
||||
pod.Name = name
|
||||
pod.UID = types.UID(uuid.New().String())
|
||||
return pod
|
||||
}
|
||||
|
||||
func TestResourceManagerUpdatePod(t *testing.T) {
|
||||
pm := NewResourceManager(fakeClient)
|
||||
pod1Name := "Pod1"
|
||||
pod1 := makePod(pod1Name)
|
||||
pm.AddPod(pod1)
|
||||
|
||||
pods := pm.GetPods()
|
||||
if len(pods) != 1 {
|
||||
t.Errorf("Got %d, expected 1 pod", len(pods))
|
||||
}
|
||||
gotPod1 := pm.GetPod(pod1Name)
|
||||
if gotPod1.Name != pod1.Name {
|
||||
t.Errorf("Got %s, wanted %s", gotPod1.Name, pod1.Name)
|
||||
}
|
||||
|
||||
if gotPod1.Namespace != "" {
|
||||
t.Errorf("Got %s, wanted %s", gotPod1.Namespace, "<empty namespace>")
|
||||
}
|
||||
pod1.Namespace = "POD1NAMESPACE"
|
||||
pm.UpdatePod(pod1)
|
||||
|
||||
gotPod1 = pm.GetPod(pod1Name)
|
||||
if gotPod1.Name != pod1.Name {
|
||||
t.Errorf("Got %s, wanted %s", gotPod1.Name, pod1.Name)
|
||||
}
|
||||
|
||||
if gotPod1.Namespace != pod1.Namespace {
|
||||
t.Errorf("Got %s, wanted %s", gotPod1.Namespace, pod1.Namespace)
|
||||
}
|
||||
}
|
||||
640
providers/azure/aci.go
Normal file
640
providers/azure/aci.go
Normal file
@@ -0,0 +1,640 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/manager"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/aci"
|
||||
"k8s.io/api/core/v1"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// ACIProvider implements the virtual-kubelet provider interface and communicates with Azure's ACI APIs.
|
||||
type ACIProvider struct {
|
||||
aciClient *aci.Client
|
||||
resourceManager *manager.ResourceManager
|
||||
resourceGroup string
|
||||
region string
|
||||
nodeName string
|
||||
operatingSystem string
|
||||
cpu string
|
||||
memory string
|
||||
pods string
|
||||
}
|
||||
|
||||
// AuthConfig is the secret returned from an ImageRegistryCredential
|
||||
type AuthConfig struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
ServerAddress string `json:"serveraddress,omitempty"`
|
||||
IdentityToken string `json:"identitytoken,omitempty"`
|
||||
RegistryToken string `json:"registrytoken,omitempty"`
|
||||
}
|
||||
|
||||
// NewACIProvider creates a new ACIProvider.
|
||||
func NewACIProvider(config string, rm *manager.ResourceManager, nodeName, operatingSystem string) (*ACIProvider, error) {
|
||||
var p ACIProvider
|
||||
var err error
|
||||
|
||||
p.resourceManager = rm
|
||||
|
||||
p.aciClient, err = aci.NewClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config != "" {
|
||||
f, err := os.Open(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := p.loadConfig(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if rg := os.Getenv("ACI_RESOURCE_GROUP"); rg != "" {
|
||||
p.resourceGroup = rg
|
||||
}
|
||||
if p.resourceGroup == "" {
|
||||
return nil, errors.New("Resource group can not be empty please set ACI_RESOURCE_GROUP")
|
||||
}
|
||||
|
||||
if r := os.Getenv("ACI_REGION"); r != "" {
|
||||
p.region = r
|
||||
}
|
||||
if p.region == "" {
|
||||
return nil, errors.New("Region can not be empty please set ACI_REGION")
|
||||
}
|
||||
|
||||
// Set sane defaults for Capacity in case config is not supplied
|
||||
p.cpu = "20"
|
||||
p.memory = "100Gi"
|
||||
p.pods = "20"
|
||||
|
||||
p.operatingSystem = operatingSystem
|
||||
p.nodeName = nodeName
|
||||
|
||||
return &p, err
|
||||
}
|
||||
|
||||
// CreatePod accepts a Pod definition and creates
|
||||
// an ACI deployment
|
||||
func (p *ACIProvider) CreatePod(pod *v1.Pod) error {
|
||||
var containerGroup aci.ContainerGroup
|
||||
containerGroup.Location = p.region
|
||||
containerGroup.RestartPolicy = aci.ContainerGroupRestartPolicy(pod.Spec.RestartPolicy)
|
||||
containerGroup.ContainerGroupProperties.OsType = aci.OperatingSystemTypes(p.OperatingSystem())
|
||||
|
||||
// get containers
|
||||
containers, err := p.getContainers(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get registry creds
|
||||
creds, err := p.getImagePullSecrets(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// get volumes
|
||||
volumes, err := p.getVolumes(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// assign all the things
|
||||
containerGroup.ContainerGroupProperties.Containers = containers
|
||||
containerGroup.ContainerGroupProperties.Volumes = volumes
|
||||
containerGroup.ContainerGroupProperties.ImageRegistryCredentials = creds
|
||||
|
||||
podUID := string(pod.UID)
|
||||
podCreationTimestamp := pod.CreationTimestamp.String()
|
||||
containerGroup.Tags = map[string]string{
|
||||
"ClusterName": pod.ClusterName,
|
||||
"NodeName": pod.Spec.NodeName,
|
||||
"Namespace": pod.Namespace,
|
||||
"UID": podUID,
|
||||
"CreationTimestamp": podCreationTimestamp,
|
||||
}
|
||||
|
||||
// TODO(BJK) containergrouprestartpolicy??
|
||||
_, err = p.aciClient.CreateContainerGroup(
|
||||
p.resourceGroup,
|
||||
pod.Name,
|
||||
containerGroup,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePod is a noop, ACI currently does not support live updates of a pod.
|
||||
func (p *ACIProvider) UpdatePod(pod *v1.Pod) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePod deletes the specified pod out of ACI.
|
||||
func (p *ACIProvider) DeletePod(pod *v1.Pod) error {
|
||||
return p.aciClient.DeleteContainerGroup(p.resourceGroup, pod.Name)
|
||||
}
|
||||
|
||||
// GetPod returns a pod by name that is running inside ACI
|
||||
// returns nil if a pod by that name is not found.
|
||||
func (p *ACIProvider) GetPod(name string) (*v1.Pod, error) {
|
||||
cg, err := p.aciClient.GetContainerGroup(p.resourceGroup, name)
|
||||
if err != nil {
|
||||
// Trap error for 404 and return gracefully
|
||||
if strings.Contains(err.Error(), "ResourceNotFound") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cg.Tags["NodeName"] != p.nodeName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return containerGroupToPod(cg)
|
||||
}
|
||||
|
||||
// GetPodStatus returns the status of a pod by name that is running inside ACI
|
||||
// returns nil if a pod by that name is not found.
|
||||
func (p *ACIProvider) GetPodStatus(name string) (*v1.PodStatus, error) {
|
||||
pod, err := p.GetPod(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pod == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &pod.Status, nil
|
||||
}
|
||||
|
||||
// GetPods returns a list of all pods known to be running within ACI.
|
||||
func (p *ACIProvider) GetPods() ([]*v1.Pod, error) {
|
||||
cgs, err := p.aciClient.ListContainerGroups(p.resourceGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pods := make([]*v1.Pod, 0, len(cgs.Value))
|
||||
|
||||
for _, cg := range cgs.Value {
|
||||
c := cg
|
||||
if cg.Tags["NodeName"] != p.nodeName {
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := containerGroupToPod(&c)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
pods = append(pods, p)
|
||||
}
|
||||
|
||||
return pods, nil
|
||||
}
|
||||
|
||||
// Capacity returns a resource list containing the capacity limits set for ACI.
|
||||
func (p *ACIProvider) Capacity() v1.ResourceList {
|
||||
return v1.ResourceList{
|
||||
"cpu": resource.MustParse(p.cpu),
|
||||
"memory": resource.MustParse(p.memory),
|
||||
"pods": resource.MustParse(p.pods),
|
||||
}
|
||||
}
|
||||
|
||||
// NodeConditions returns a list of conditions (Ready, OutOfDisk, etc), for updates to the node status
|
||||
// within Kuberentes.
|
||||
func (p *ACIProvider) NodeConditions() []v1.NodeCondition {
|
||||
// TODO: Make these dynamic and augment with custom ACI specific conditions of interest
|
||||
return []v1.NodeCondition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: v1.ConditionTrue,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletReady",
|
||||
Message: "kubelet is ready.",
|
||||
},
|
||||
{
|
||||
Type: "OutOfDisk",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasSufficientDisk",
|
||||
Message: "kubelet has sufficient disk space available",
|
||||
},
|
||||
{
|
||||
Type: "MemoryPressure",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasSufficientMemory",
|
||||
Message: "kubelet has sufficient memory available",
|
||||
},
|
||||
{
|
||||
Type: "DiskPressure",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasNoDiskPressure",
|
||||
Message: "kubelet has no disk pressure",
|
||||
},
|
||||
{
|
||||
Type: "NetworkUnavailable",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "RouteCreated",
|
||||
Message: "RouteController created a route",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OperatingSystem returns the operating system that was provided by the config.
|
||||
func (p *ACIProvider) OperatingSystem() string {
|
||||
return p.operatingSystem
|
||||
}
|
||||
|
||||
func (p *ACIProvider) getImagePullSecrets(pod *v1.Pod) ([]aci.ImageRegistryCredential, error) {
|
||||
ips := make([]aci.ImageRegistryCredential, 0, len(pod.Spec.ImagePullSecrets))
|
||||
for _, ref := range pod.Spec.ImagePullSecrets {
|
||||
secret, err := p.resourceManager.GetSecret(ref.Name, pod.Namespace)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
if secret == nil {
|
||||
return nil, fmt.Errorf("error getting image pull secret")
|
||||
}
|
||||
// TODO: Check if secret type is v1.SecretTypeDockercfg and use DockerConfigKey instead of hardcoded value
|
||||
// TODO: Check if secret type is v1.SecretTypeDockerConfigJson and use DockerConfigJsonKey to determine if it's in json format
|
||||
// TODO: Return error if it's not one of these two types
|
||||
repoDataB64, ok := secret.Data[".dockercfg"]
|
||||
if !ok {
|
||||
return ips, fmt.Errorf("no dockercfg present in secret")
|
||||
}
|
||||
repoData, err := base64.StdEncoding.DecodeString(string(repoDataB64))
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
var ac AuthConfig
|
||||
err = json.Unmarshal(repoData, &ac)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
ips = append(ips, aci.ImageRegistryCredential{
|
||||
Password: ac.Password,
|
||||
Server: ac.ServerAddress,
|
||||
Username: ac.Username,
|
||||
})
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func (p *ACIProvider) getContainers(pod *v1.Pod) ([]aci.Container, error) {
|
||||
containers := make([]aci.Container, 0, len(pod.Spec.Containers))
|
||||
for _, container := range pod.Spec.Containers {
|
||||
c := aci.Container{
|
||||
Name: container.Name,
|
||||
ContainerProperties: aci.ContainerProperties{
|
||||
Image: container.Image,
|
||||
Command: container.Command,
|
||||
Ports: make([]aci.ContainerPort, 0, len(container.Ports)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range container.Ports {
|
||||
c.Ports = append(c.Ports, aci.ContainerPort{
|
||||
Port: p.HostPort,
|
||||
Protocol: getProtocol(p.Protocol),
|
||||
})
|
||||
}
|
||||
|
||||
c.VolumeMounts = make([]aci.VolumeMount, 0, len(container.VolumeMounts))
|
||||
for _, v := range container.VolumeMounts {
|
||||
c.VolumeMounts = append(c.VolumeMounts, aci.VolumeMount{
|
||||
Name: v.Name,
|
||||
MountPath: v.MountPath,
|
||||
ReadOnly: v.ReadOnly,
|
||||
})
|
||||
}
|
||||
|
||||
c.EnvironmentVariables = make([]aci.EnvironmentVariable, 0, len(container.Env))
|
||||
for _, e := range container.Env {
|
||||
c.EnvironmentVariables = append(c.EnvironmentVariables, aci.EnvironmentVariable{
|
||||
Name: e.Name,
|
||||
Value: e.Value,
|
||||
})
|
||||
}
|
||||
|
||||
cpuLimit := float64(container.Resources.Limits.Cpu().Value())
|
||||
memoryLimit := float64(container.Resources.Limits.Memory().Value()) / 1000000000.00
|
||||
cpuRequest := float64(container.Resources.Requests.Cpu().Value())
|
||||
memoryRequest := float64(container.Resources.Requests.Memory().Value()) / 1000000000.00
|
||||
|
||||
c.Resources = aci.ResourceRequirements{
|
||||
Limits: aci.ResourceLimits{
|
||||
CPU: cpuLimit,
|
||||
MemoryInGB: memoryLimit,
|
||||
},
|
||||
Requests: aci.ResourceRequests{
|
||||
CPU: cpuRequest,
|
||||
MemoryInGB: memoryRequest,
|
||||
},
|
||||
}
|
||||
|
||||
containers = append(containers, c)
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (p *ACIProvider) getVolumes(pod *v1.Pod) ([]aci.Volume, error) {
|
||||
volumes := make([]aci.Volume, 0, len(pod.Spec.Volumes))
|
||||
for _, v := range pod.Spec.Volumes {
|
||||
// Handle the case for the AzureFile volume.
|
||||
if v.AzureFile != nil {
|
||||
secret, err := p.resourceManager.GetSecret(v.AzureFile.SecretName, pod.Namespace)
|
||||
if err != nil {
|
||||
return volumes, err
|
||||
}
|
||||
|
||||
if secret == nil {
|
||||
return nil, fmt.Errorf("Getting secret for AzureFile volume returned an empty secret.")
|
||||
}
|
||||
|
||||
volumes = append(volumes, aci.Volume{
|
||||
Name: v.Name,
|
||||
AzureFile: &aci.AzureFileVolume{
|
||||
ShareName: v.AzureFile.ShareName,
|
||||
ReadOnly: v.AzureFile.ReadOnly,
|
||||
StorageAccountName: string(secret.Data["StorageAccountName"]),
|
||||
StorageAccountKey: string(secret.Data["StorageAccountKey"]),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the case for the EmptyDir.
|
||||
if v.EmptyDir != nil {
|
||||
volumes = append(volumes, aci.Volume{
|
||||
Name: v.Name,
|
||||
EmptyDir: map[string]interface{}{
|
||||
"medium": v.EmptyDir.Medium,
|
||||
"sizeLimit": v.EmptyDir.SizeLimit,
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the case for GitRepo volume.
|
||||
if v.GitRepo != nil {
|
||||
volumes = append(volumes, aci.Volume{
|
||||
Name: v.Name,
|
||||
GitRepo: &aci.GitRepoVolume{
|
||||
Directory: v.GitRepo.Directory,
|
||||
Repository: v.GitRepo.Repository,
|
||||
Revision: v.GitRepo.Revision,
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the case for Secret volume.
|
||||
if v.Secret != nil {
|
||||
paths := make(map[string]string)
|
||||
secret, err := p.resourceManager.GetSecret(v.Secret.SecretName, pod.Namespace)
|
||||
if v.Secret.Optional != nil && !*v.Secret.Optional && k8serr.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("Secret %s is required by Pod %s and does not exist", v.Secret.SecretName, pod.Name)
|
||||
}
|
||||
if secret == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range secret.Data {
|
||||
var b bytes.Buffer
|
||||
enc := base64.NewEncoder(base64.StdEncoding, &b)
|
||||
enc.Write(v)
|
||||
|
||||
paths[k] = b.String()
|
||||
}
|
||||
|
||||
if len(paths) != 0 {
|
||||
volumes = append(volumes, aci.Volume{
|
||||
Name: v.Name,
|
||||
Secret: paths,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle the case for ConfigMap volume.
|
||||
if v.ConfigMap != nil {
|
||||
paths := make(map[string]string)
|
||||
configMap, err := p.resourceManager.GetConfigMap(v.ConfigMap.Name, pod.Namespace)
|
||||
if v.Secret.Optional != nil && !*v.Secret.Optional && k8serr.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("ConfigMap %s is required by Pod %s and does not exist", v.ConfigMap.Name, pod.Name)
|
||||
}
|
||||
if configMap == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range configMap.Data {
|
||||
var b bytes.Buffer
|
||||
enc := base64.NewEncoder(base64.StdEncoding, &b)
|
||||
enc.Write([]byte(v))
|
||||
|
||||
paths[k] = b.String()
|
||||
}
|
||||
|
||||
if len(paths) != 0 {
|
||||
volumes = append(volumes, aci.Volume{
|
||||
Name: v.Name,
|
||||
Secret: paths,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If we've made it this far we have found a volume type that isn't supported
|
||||
return nil, fmt.Errorf("Pod %s requires volume %s which is of an unsupported type", pod.Name, v.Name)
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
func getProtocol(pro v1.Protocol) aci.ContainerNetworkProtocol {
|
||||
switch pro {
|
||||
case v1.ProtocolUDP:
|
||||
return aci.ContainerNetworkProtocolUDP
|
||||
default:
|
||||
return aci.ContainerNetworkProtocolTCP
|
||||
}
|
||||
}
|
||||
|
||||
func containerGroupToPod(cg *aci.ContainerGroup) (*v1.Pod, error) {
|
||||
var podCreationTimestamp metav1.Time
|
||||
|
||||
if cg.Tags["CreationTimestamp"] != "" {
|
||||
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", cg.Tags["CreationTimestamp"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podCreationTimestamp = metav1.NewTime(t)
|
||||
}
|
||||
containerStartTime := metav1.NewTime(time.Time(cg.Containers[0].ContainerProperties.InstanceView.CurrentState.StartTime))
|
||||
|
||||
// Use the Provisioning State if it's not Succeeded,
|
||||
// otherwise use the state of the instance.
|
||||
aciState := cg.ContainerGroupProperties.ProvisioningState
|
||||
if aciState == "Succeeded" {
|
||||
aciState = cg.ContainerGroupProperties.InstanceView.State
|
||||
}
|
||||
|
||||
containers := make([]v1.Container, 0, len(cg.Containers))
|
||||
containerStatuses := make([]v1.ContainerStatus, 0, len(cg.Containers))
|
||||
for _, c := range cg.Containers {
|
||||
container := v1.Container{
|
||||
Name: c.Name,
|
||||
Image: c.Image,
|
||||
Command: c.Command,
|
||||
Resources: v1.ResourceRequirements{
|
||||
Limits: v1.ResourceList{
|
||||
v1.ResourceCPU: resource.MustParse(fmt.Sprintf("%d", int64(c.Resources.Limits.CPU))),
|
||||
v1.ResourceMemory: resource.MustParse(fmt.Sprintf("%gG", c.Resources.Limits.MemoryInGB)),
|
||||
},
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceCPU: resource.MustParse(fmt.Sprintf("%d", int64(c.Resources.Requests.CPU))),
|
||||
v1.ResourceMemory: resource.MustParse(fmt.Sprintf("%gG", c.Resources.Requests.MemoryInGB)),
|
||||
},
|
||||
},
|
||||
}
|
||||
containers = append(containers, container)
|
||||
containerStatus := v1.ContainerStatus{
|
||||
Name: c.Name,
|
||||
State: aciContainerStateToContainerState(c.InstanceView.CurrentState),
|
||||
LastTerminationState: aciContainerStateToContainerState(c.InstanceView.PreviousState),
|
||||
Ready: aciStateToPodPhase(c.InstanceView.CurrentState.State) == v1.PodRunning,
|
||||
RestartCount: c.InstanceView.RestartCount,
|
||||
Image: c.Image,
|
||||
ImageID: "",
|
||||
ContainerID: "",
|
||||
}
|
||||
|
||||
// Add to containerStatuses
|
||||
containerStatuses = append(containerStatuses, containerStatus)
|
||||
}
|
||||
|
||||
ip := ""
|
||||
if cg.IPAddress != nil {
|
||||
ip = cg.IPAddress.IP
|
||||
}
|
||||
|
||||
p := v1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Pod",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cg.Name,
|
||||
Namespace: cg.Tags["Namespace"],
|
||||
ClusterName: cg.Tags["ClusterName"],
|
||||
UID: types.UID(cg.Tags["UID"]),
|
||||
CreationTimestamp: podCreationTimestamp,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
NodeName: cg.Tags["NodeName"],
|
||||
Volumes: []v1.Volume{},
|
||||
Containers: containers,
|
||||
},
|
||||
// TODO: Make this dynamic, likely can translate the provisioningState or instanceView.ContainerState into a Phase,
|
||||
// and some of the Events into Conditions
|
||||
Status: v1.PodStatus{
|
||||
Phase: aciStateToPodPhase(aciState),
|
||||
Conditions: []v1.PodCondition{},
|
||||
Message: "",
|
||||
Reason: "",
|
||||
HostIP: "",
|
||||
PodIP: ip,
|
||||
StartTime: &containerStartTime,
|
||||
ContainerStatuses: containerStatuses,
|
||||
},
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func aciStateToPodPhase(state string) v1.PodPhase {
|
||||
switch state {
|
||||
case "Running":
|
||||
return v1.PodRunning
|
||||
case "Succeeded":
|
||||
return v1.PodSucceeded
|
||||
case "Failed":
|
||||
return v1.PodFailed
|
||||
case "Canceled":
|
||||
return v1.PodFailed
|
||||
case "Creating":
|
||||
return v1.PodPending
|
||||
case "Repairing":
|
||||
return v1.PodPending
|
||||
case "Pending":
|
||||
return v1.PodPending
|
||||
case "Accepted":
|
||||
return v1.PodPending
|
||||
}
|
||||
|
||||
return v1.PodUnknown
|
||||
}
|
||||
|
||||
func aciContainerStateToContainerState(cs aci.ContainerState) v1.ContainerState {
|
||||
startTime := metav1.NewTime(time.Time(cs.StartTime))
|
||||
|
||||
// Handle the case where the container is running.
|
||||
if cs.State == "Running" || cs.State == "Succeeded" {
|
||||
return v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{
|
||||
StartedAt: startTime,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the case where the container failed.
|
||||
if cs.State == "Failed" || cs.State == "Canceled" {
|
||||
return v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{
|
||||
ExitCode: cs.ExitCode,
|
||||
Reason: cs.State,
|
||||
Message: cs.DetailStatus,
|
||||
StartedAt: startTime,
|
||||
FinishedAt: metav1.NewTime(time.Time(cs.FinishTime)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the case where the container is pending.
|
||||
// Which should be all other aci states.
|
||||
return v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
Reason: cs.State,
|
||||
Message: cs.DetailStatus,
|
||||
},
|
||||
}
|
||||
}
|
||||
40
providers/azure/client/README.md
Normal file
40
providers/azure/client/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# A half-baked SDK for Azure in Go
|
||||
|
||||
This is a half-baked (ie. only provides what we needed) SDK for Azure in Go.
|
||||
|
||||
## Authentication
|
||||
|
||||
### Use an authentication file
|
||||
|
||||
This SDK also supports authentication with a JSON file containing credentials for the service principal. In the Azure CLI, you can create a service principal and its authentication file with this command:
|
||||
|
||||
``` bash
|
||||
az ad sp create-for-rbac --sdk-auth > mycredentials.json
|
||||
```
|
||||
|
||||
Save this file in a secure location on your system where your code can read it. Set an environment variable with the full path to the file:
|
||||
|
||||
``` bash
|
||||
export AZURE_AUTH_LOCATION=/secure/location/mycredentials.json
|
||||
```
|
||||
|
||||
``` powershell
|
||||
$env:AZURE_AUTH_LOCATION= "/secure/location/mycredentials.json"
|
||||
```
|
||||
|
||||
The file looks like this, in case you want to create it yourself:
|
||||
|
||||
``` json
|
||||
{
|
||||
"clientId": "<your service principal client ID>",
|
||||
"clientSecret": "your service principal client secret",
|
||||
"subscriptionId": "<your Azure Subsription ID>",
|
||||
"tenantId": "<your tenant ID>",
|
||||
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
|
||||
"resourceManagerEndpointUrl": "https://management.azure.com/",
|
||||
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
|
||||
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
|
||||
"galleryEndpointUrl": "https://gallery.azure.com/",
|
||||
"managementEndpointUrl": "https://management.core.windows.net/"
|
||||
}
|
||||
```
|
||||
45
providers/azure/client/aci/client.go
Normal file
45
providers/azure/client/aci/client.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
azure "github.com/virtual-kubelet/virtual-kubelet/providers/azure/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// BaseURI is the default URI used for compute services.
|
||||
BaseURI = "https://management.azure.com"
|
||||
userAgent = "virtual-kubelet/azure-arm-aci/2017-12-01"
|
||||
apiVersion = "2017-12-01-preview"
|
||||
|
||||
containerGroupURLPath = "subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups/{{.containerGroupName}}"
|
||||
containerGroupListURLPath = "subscriptions/{{.subscriptionId}}/providers/Microsoft.ContainerInstance/containerGroups"
|
||||
containerGroupListByResourceGroupURLPath = "subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups"
|
||||
containerLogsURLPath = containerGroupURLPath + "/containers/{{.containerName}}/logs"
|
||||
)
|
||||
|
||||
// Client is a client for interacting with Azure Container Instances.
|
||||
//
|
||||
// Clients should be reused instead of created as needed.
|
||||
// The methods of Client are safe for concurrent use by multiple goroutines.
|
||||
type Client struct {
|
||||
hc *http.Client
|
||||
auth *azure.Authentication
|
||||
}
|
||||
|
||||
// NewClient creates a new Azure Container Instances client.
|
||||
func NewClient() (*Client, error) {
|
||||
// Get authentication credentials from file.
|
||||
auth, err := azure.NewAuthenticationFromFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating azure authentication from file failed: %v", err)
|
||||
}
|
||||
|
||||
client, err := azure.NewClient(auth, BaseURI, userAgent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating azure client failed: %v", err)
|
||||
}
|
||||
|
||||
return &Client{hc: client.HTTPClient, auth: auth}, nil
|
||||
}
|
||||
192
providers/azure/client/aci/client_test.go
Normal file
192
providers/azure/client/aci/client_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/resourcegroups"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
client *Client
|
||||
location = "eastus"
|
||||
resourceGroup = "virtual-kubelet-tests"
|
||||
containerGroup = "virtual-kubelet-test-container-group"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check if the AZURE_AUTH_LOCATION variable is already set.
|
||||
// If it is not set, set it to the root of this project in a credentials.json file.
|
||||
if os.Getenv("AZURE_AUTH_LOCATION") == "" {
|
||||
// Check if the credentials.json file exists in the root of this project.
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
dir := filepath.Dir(filename)
|
||||
file := filepath.Join(dir, "../../../../credentials.json")
|
||||
|
||||
// Check if the file exists.
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
log.Fatalf("Either set AZURE_AUTH_LOCATION or add a credentials.json file to the root of this project.")
|
||||
}
|
||||
|
||||
// Set the environment variable for the authentication file.
|
||||
os.Setenv("AZURE_AUTH_LOCATION", file)
|
||||
}
|
||||
|
||||
// Create a resource group name with uuid.
|
||||
uid := uuid.New()
|
||||
resourceGroup += "-" + uid.String()[0:6]
|
||||
}
|
||||
|
||||
// The TestMain function creates a resource group for testing
|
||||
// and deletes in when it's done.
|
||||
func TestMain(m *testing.M) {
|
||||
// Check if the resource group exists and create it if not.
|
||||
rgCli, err := resourcegroups.NewClient()
|
||||
if err != nil {
|
||||
log.Fatalf("creating new resourcegroups client failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if the resource group exists.
|
||||
exists, err := rgCli.ResourceGroupExists(resourceGroup)
|
||||
if err != nil {
|
||||
log.Fatalf("checking if resource group exists failed: %v", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Create the resource group.
|
||||
_, err := rgCli.CreateResourceGroup(resourceGroup, resourcegroups.Group{
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("creating resource group failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the tests.
|
||||
merr := m.Run()
|
||||
|
||||
// Delete the resource group.
|
||||
if err := rgCli.DeleteResourceGroup(resourceGroup); err != nil {
|
||||
log.Printf("Couldn't delete resource group %q: %v", resourceGroup, err)
|
||||
|
||||
}
|
||||
|
||||
if merr != 0 {
|
||||
os.Exit(merr)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
var err error
|
||||
client, err = NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateContainerGroupFails(t *testing.T) {
|
||||
_, err := client.CreateContainerGroup(resourceGroup, containerGroup, ContainerGroup{
|
||||
Location: location,
|
||||
ContainerGroupProperties: ContainerGroupProperties{
|
||||
OsType: Linux,
|
||||
Containers: []Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
ContainerProperties: ContainerProperties{
|
||||
Image: "nginx",
|
||||
Command: []string{"nginx", "-g", "daemon off;"},
|
||||
Ports: []ContainerPort{
|
||||
{
|
||||
Protocol: ContainerNetworkProtocolTCP,
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected create container group to fail with ResourceSomeRequestsNotSpecified, but returned nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "ResourceSomeRequestsNotSpecified") {
|
||||
t.Fatalf("expected ResourceSomeRequestsNotSpecified to be in the error message but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateContainerGroup(t *testing.T) {
|
||||
cg, err := client.CreateContainerGroup(resourceGroup, containerGroup, ContainerGroup{
|
||||
Location: location,
|
||||
ContainerGroupProperties: ContainerGroupProperties{
|
||||
OsType: Linux,
|
||||
Containers: []Container{
|
||||
{
|
||||
Name: "nginx",
|
||||
ContainerProperties: ContainerProperties{
|
||||
Image: "nginx",
|
||||
Command: []string{"nginx", "-g", "daemon off;"},
|
||||
Ports: []ContainerPort{
|
||||
{
|
||||
Protocol: ContainerNetworkProtocolTCP,
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
Resources: ResourceRequirements{
|
||||
Requests: ResourceRequests{
|
||||
CPU: 1,
|
||||
MemoryInGB: 1,
|
||||
},
|
||||
Limits: ResourceLimits{
|
||||
CPU: 1,
|
||||
MemoryInGB: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cg.Name != containerGroup {
|
||||
t.Fatalf("resource group name is %s, expected %s", cg.Name, containerGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetContainerGroup(t *testing.T) {
|
||||
cg, err := client.GetContainerGroup(resourceGroup, containerGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cg.Name != containerGroup {
|
||||
t.Fatalf("resource group name is %s, expected %s", cg.Name, containerGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListContainerGroup(t *testing.T) {
|
||||
list, err := client.ListContainerGroups(resourceGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, cg := range list.Value {
|
||||
if cg.Name != containerGroup {
|
||||
t.Fatalf("resource group name is %s, expected %s", cg.Name, containerGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteContainerGroup(t *testing.T) {
|
||||
err := client.DeleteContainerGroup(resourceGroup, containerGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
69
providers/azure/client/aci/create.go
Normal file
69
providers/azure/client/aci/create.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// CreateContainerGroup creates a new Azure Container Instance with the
|
||||
// provided properties.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/createorupdate
|
||||
func (c *Client) CreateContainerGroup(resourceGroup, containerGroupName string, containerGroup ContainerGroup) (*ContainerGroup, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, containerGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the body for the request.
|
||||
b := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(b).Encode(containerGroup); err != nil {
|
||||
return nil, fmt.Errorf("Encoding create container group body request failed: %v", err)
|
||||
}
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("PUT", uri, b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating create/update container group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroup": resourceGroup,
|
||||
"containerGroupName": containerGroupName,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending create container group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) and 201 (Created) are a successful responses.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create container group returned an empty body in the response")
|
||||
}
|
||||
var cg ContainerGroup
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cg); err != nil {
|
||||
return nil, fmt.Errorf("Decoding create container group response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &cg, nil
|
||||
}
|
||||
55
providers/azure/client/aci/delete.go
Normal file
55
providers/azure/client/aci/delete.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// DeleteContainerGroup deletes an Azure Container Instance in the provided
|
||||
// resource group with the given container group name.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/delete
|
||||
func (c *Client) DeleteContainerGroup(resourceGroup, containerGroupName string) error {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, containerGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("DELETE", uri, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Creating delete container group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroup": resourceGroup,
|
||||
"containerGroupName": containerGroupName,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Sending delete container group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 204 No Content means the specified container group was not found.
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return fmt.Errorf("Container group with name %q was not found", containerGroupName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
2
providers/azure/client/aci/doc.go
Normal file
2
providers/azure/client/aci/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package aci provides tools for interacting with the Azure Container Instances API.
|
||||
package aci
|
||||
62
providers/azure/client/aci/get.go
Normal file
62
providers/azure/client/aci/get.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// GetContainerGroup gets an Azure Container Instance in the provided
|
||||
// resource group with the given container group name.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/get
|
||||
func (c *Client) GetContainerGroup(resourceGroup, containerGroupName string) (*ContainerGroup, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, containerGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating get container group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroup": resourceGroup,
|
||||
"containerGroupName": containerGroupName,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending get container group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) is a success response.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create container group returned an empty body in the response")
|
||||
}
|
||||
var cg ContainerGroup
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cg); err != nil {
|
||||
return nil, fmt.Errorf("Decoding get container group response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &cg, nil
|
||||
}
|
||||
69
providers/azure/client/aci/list.go
Normal file
69
providers/azure/client/aci/list.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// ListContainerGroups lists an Azure Container Instance Groups, if a resource
|
||||
// group is given it will list by resource group.
|
||||
// It optionally accepts a resource group name and will filter based off of it
|
||||
// if it is not empty.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/list
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/listbyresourcegroup
|
||||
func (c *Client) ListContainerGroups(resourceGroup string) (*ContainerGroupListResult, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, containerGroupListURLPath)
|
||||
// List by resource group if they passed one.
|
||||
if resourceGroup != "" {
|
||||
uri = api.ResolveRelative(BaseURI, containerGroupListByResourceGroupURLPath)
|
||||
|
||||
}
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating get container group list uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroup": resourceGroup,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending get container group list request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) is a success response.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create container group list returned an empty body in the response")
|
||||
}
|
||||
var list ContainerGroupListResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
|
||||
return nil, fmt.Errorf("Decoding get container group response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &list, nil
|
||||
}
|
||||
64
providers/azure/client/aci/logs.go
Normal file
64
providers/azure/client/aci/logs.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// GetContainerLogs returns the logs from an Azure Container Instance
|
||||
// in the provided resource group with the given container group name.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/ContainerLogs/List
|
||||
func (c *Client) GetContainerLogs(resourceGroup, containerGroupName, containerName string, tail int) (*Logs, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
"tail": []string{fmt.Sprintf("%d", tail)},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, containerLogsURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating get container logs uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroup": resourceGroup,
|
||||
"containerGroupName": containerGroupName,
|
||||
"containerName": containerName,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending get container logs request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) is a success response.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create container logs returned an empty body in the response")
|
||||
}
|
||||
var logs Logs
|
||||
if err := json.NewDecoder(resp.Body).Decode(&logs); err != nil {
|
||||
return nil, fmt.Errorf("Decoding get container logs response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &logs, nil
|
||||
}
|
||||
276
providers/azure/client/aci/types.go
Normal file
276
providers/azure/client/aci/types.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package aci
|
||||
|
||||
import (
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// ContainerGroupNetworkProtocol enumerates the values for container group network protocol.
|
||||
type ContainerGroupNetworkProtocol string
|
||||
|
||||
const (
|
||||
// TCP specifies the tcp state for container group network protocol.
|
||||
TCP ContainerGroupNetworkProtocol = "TCP"
|
||||
// UDP specifies the udp state for container group network protocol.
|
||||
UDP ContainerGroupNetworkProtocol = "UDP"
|
||||
)
|
||||
|
||||
// ContainerGroupRestartPolicy enumerates the values for container group restart policy.
|
||||
type ContainerGroupRestartPolicy string
|
||||
|
||||
const (
|
||||
// Always specifies the always state for container group restart policy.
|
||||
Always ContainerGroupRestartPolicy = "Always"
|
||||
// Never specifies the never state for container group restart policy.
|
||||
Never ContainerGroupRestartPolicy = "Never"
|
||||
// OnFailure specifies the on failure state for container group restart policy.
|
||||
OnFailure ContainerGroupRestartPolicy = "OnFailure"
|
||||
)
|
||||
|
||||
// ContainerNetworkProtocol enumerates the values for container network protocol.
|
||||
type ContainerNetworkProtocol string
|
||||
|
||||
const (
|
||||
// ContainerNetworkProtocolTCP specifies the container network protocol tcp state for container network protocol.
|
||||
ContainerNetworkProtocolTCP ContainerNetworkProtocol = "TCP"
|
||||
// ContainerNetworkProtocolUDP specifies the container network protocol udp state for container network protocol.
|
||||
ContainerNetworkProtocolUDP ContainerNetworkProtocol = "UDP"
|
||||
)
|
||||
|
||||
// OperatingSystemTypes enumerates the values for operating system types.
|
||||
type OperatingSystemTypes string
|
||||
|
||||
const (
|
||||
// Linux specifies the linux state for operating system types.
|
||||
Linux OperatingSystemTypes = "Linux"
|
||||
// Windows specifies the windows state for operating system types.
|
||||
Windows OperatingSystemTypes = "Windows"
|
||||
)
|
||||
|
||||
// OperationsOrigin enumerates the values for operations origin.
|
||||
type OperationsOrigin string
|
||||
|
||||
const (
|
||||
// System specifies the system state for operations origin.
|
||||
System OperationsOrigin = "System"
|
||||
// User specifies the user state for operations origin.
|
||||
User OperationsOrigin = "User"
|
||||
)
|
||||
|
||||
// AzureFileVolume is the properties of the Azure File volume. Azure File shares are mounted as volumes.
|
||||
type AzureFileVolume struct {
|
||||
ShareName string `json:"shareName,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
StorageAccountName string `json:"storageAccountName,omitempty"`
|
||||
StorageAccountKey string `json:"storageAccountKey,omitempty"`
|
||||
}
|
||||
|
||||
// Container is a container instance.
|
||||
type Container struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ContainerProperties `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerGroup is a container group.
|
||||
type ContainerGroup struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
ContainerGroupProperties `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerGroupProperties is
|
||||
type ContainerGroupProperties struct {
|
||||
ProvisioningState string `json:"provisioningState,omitempty"`
|
||||
Containers []Container `json:"containers,omitempty"`
|
||||
ImageRegistryCredentials []ImageRegistryCredential `json:"imageRegistryCredentials,omitempty"`
|
||||
RestartPolicy ContainerGroupRestartPolicy `json:"restartPolicy,omitempty"`
|
||||
IPAddress *IPAddress `json:"ipAddress,omitempty"`
|
||||
OsType OperatingSystemTypes `json:"osType,omitempty"`
|
||||
Volumes []Volume `json:"volumes,omitempty"`
|
||||
InstanceView ContainerGroupPropertiesInstanceView `json:"instanceView,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerGroupPropertiesInstanceView is the instance view of the container group. Only valid in response.
|
||||
type ContainerGroupPropertiesInstanceView struct {
|
||||
Events []Event `json:"events,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerGroupListResult is the container group list response that contains the container group properties.
|
||||
type ContainerGroupListResult struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
Value []ContainerGroup `json:"value,omitempty"`
|
||||
NextLink string `json:"nextLink,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerPort is the port exposed on the container instance.
|
||||
type ContainerPort struct {
|
||||
Protocol ContainerNetworkProtocol `json:"protocol,omitempty"`
|
||||
Port int32 `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerProperties is the container instance properties.
|
||||
type ContainerProperties struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Command []string `json:"command,omitempty"`
|
||||
Ports []ContainerPort `json:"ports,omitempty"`
|
||||
EnvironmentVariables []EnvironmentVariable `json:"environmentVariables,omitempty"`
|
||||
InstanceView ContainerPropertiesInstanceView `json:"instanceView,omitempty"`
|
||||
Resources ResourceRequirements `json:"resources,omitempty"`
|
||||
VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerPropertiesInstanceView is the instance view of the container instance. Only valid in response.
|
||||
type ContainerPropertiesInstanceView struct {
|
||||
RestartCount int32 `json:"restartCount,omitempty"`
|
||||
CurrentState ContainerState `json:"currentState,omitempty"`
|
||||
PreviousState ContainerState `json:"previousState,omitempty"`
|
||||
Events []Event `json:"events,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerState is the container instance state.
|
||||
type ContainerState struct {
|
||||
State string `json:"state,omitempty"`
|
||||
StartTime api.JSONTime `json:"startTime,omitempty"`
|
||||
ExitCode int32 `json:"exitCode,omitempty"`
|
||||
FinishTime api.JSONTime `json:"finishTime,omitempty"`
|
||||
DetailStatus string `json:"detailStatus,omitempty"`
|
||||
}
|
||||
|
||||
// EnvironmentVariable is the environment variable to set within the container instance.
|
||||
type EnvironmentVariable struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Event is a container group or container instance event.
|
||||
type Event struct {
|
||||
Count int32 `json:"count,omitempty"`
|
||||
FirstTimestamp api.JSONTime `json:"firstTimestamp,omitempty"`
|
||||
LastTimestamp api.JSONTime `json:"lastTimestamp,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// GitRepoVolume is represents a volume that is populated with the contents of a git repository
|
||||
type GitRepoVolume struct {
|
||||
Directory string `json:"directory,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
}
|
||||
|
||||
// ImageRegistryCredential is image registry credential.
|
||||
type ImageRegistryCredential struct {
|
||||
Server string `json:"server,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// IPAddress is IP address for the container group.
|
||||
type IPAddress struct {
|
||||
Ports []Port `json:"ports,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
// Logs is the logs.
|
||||
type Logs struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// Operation is an operation for Azure Container Instance service.
|
||||
type Operation struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Display OperationDisplay `json:"display,omitempty"`
|
||||
Origin OperationsOrigin `json:"origin,omitempty"`
|
||||
}
|
||||
|
||||
// OperationDisplay is the display information of the operation.
|
||||
type OperationDisplay struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Operation string `json:"operation,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// OperationListResult is the operation list response that contains all operations for Azure Container Instance
|
||||
// service.
|
||||
type OperationListResult struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
Value []Operation `json:"value,omitempty"`
|
||||
NextLink string `json:"nextLink,omitempty"`
|
||||
}
|
||||
|
||||
// Port is the port exposed on the container group.
|
||||
type Port struct {
|
||||
Protocol ContainerGroupNetworkProtocol `json:"protocol,omitempty"`
|
||||
Port int32 `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// Resource is the Resource model definition.
|
||||
type Resource struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceLimits is the resource limits.
|
||||
type ResourceLimits struct {
|
||||
MemoryInGB float64 `json:"memoryInGB,omitempty"`
|
||||
CPU float64 `json:"cpu,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceRequests is the resource requests.
|
||||
type ResourceRequests struct {
|
||||
MemoryInGB float64 `json:"memoryInGB,omitempty"`
|
||||
CPU float64 `json:"cpu,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceRequirements is the resource requirements.
|
||||
type ResourceRequirements struct {
|
||||
Requests ResourceRequests `json:"requests,omitempty"`
|
||||
Limits ResourceLimits `json:"limits,omitempty"`
|
||||
}
|
||||
|
||||
// Usage is a single usage result
|
||||
type Usage struct {
|
||||
Unit string `json:"unit,omitempty"`
|
||||
CurrentValue int32 `json:"currentValue,omitempty"`
|
||||
Limit int32 `json:"limit,omitempty"`
|
||||
Name UsageName `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// UsageName is the name object of the resource
|
||||
type UsageName struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
LocalizedValue string `json:"localizedValue,omitempty"`
|
||||
}
|
||||
|
||||
// UsageListResult is the response containing the usage data
|
||||
type UsageListResult struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
Value []Usage `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Volume is the properties of the volume.
|
||||
type Volume struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
AzureFile *AzureFileVolume `json:"azureFile,omitempty"`
|
||||
EmptyDir map[string]interface{} `json:"emptyDir,omitempty"`
|
||||
Secret map[string]string `json:"secret,omitempty"`
|
||||
GitRepo *GitRepoVolume `json:"gitRepo,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeMount is the properties of the volume mount.
|
||||
type VolumeMount struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
MountPath string `json:"mountPath,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
}
|
||||
8
providers/azure/client/aci/update.go
Normal file
8
providers/azure/client/aci/update.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package aci
|
||||
|
||||
// UpdateContainerGroup updates an Azure Container Instance with the
|
||||
// provided properties.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/createorupdate
|
||||
func (c *Client) UpdateContainerGroup(resourceGroup, containerGroupName string, containerGroup ContainerGroup) (*ContainerGroup, error) {
|
||||
return c.CreateContainerGroup(resourceGroup, containerGroupName, containerGroup)
|
||||
}
|
||||
67
providers/azure/client/api/api.go
Normal file
67
providers/azure/client/api/api.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Package api contains the common code shared by all Azure API libraries.
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Error contains an error response from the server.
|
||||
type Error struct {
|
||||
// StatusCode is the HTTP response status code and will always be populated.
|
||||
StatusCode int `json:"statusCode"`
|
||||
// Code is the API error code that is given in the error message.
|
||||
Code string `json:"code"`
|
||||
// Message is the server response message and is only populated when
|
||||
// explicitly referenced by the JSON server response.
|
||||
Message string `json:"message"`
|
||||
// Body is the raw response returned by the server.
|
||||
// It is often but not always JSON, depending on how the request fails.
|
||||
Body string
|
||||
// Header contains the response header fields from the server.
|
||||
Header http.Header
|
||||
// URL is the URL of the original HTTP request and will always be populated.
|
||||
URL string
|
||||
}
|
||||
|
||||
// Error converts the Error type to a readable string.
|
||||
func (e *Error) Error() string {
|
||||
// If the message is empty return early.
|
||||
if e.Message == "" {
|
||||
return fmt.Sprintf("api call to %s: got HTTP response status code %d error code %q with body: %v", e.URL, e.StatusCode, e.Code, e.Body)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("api call to %s: got HTTP response status code %d error code %q: %s", e.URL, e.StatusCode, e.Code, e.Message)
|
||||
}
|
||||
|
||||
type errorReply struct {
|
||||
Error *Error `json:"error"`
|
||||
}
|
||||
|
||||
// CheckResponse returns an error (of type *Error) if the response
|
||||
// status code is not 2xx.
|
||||
func CheckResponse(res *http.Response) error {
|
||||
if res.StatusCode >= 200 && res.StatusCode <= 299 {
|
||||
return nil
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err == nil {
|
||||
jerr := new(errorReply)
|
||||
err = json.Unmarshal(slurp, jerr)
|
||||
if err == nil && jerr.Error != nil {
|
||||
if jerr.Error.StatusCode == 0 {
|
||||
jerr.Error.StatusCode = res.StatusCode
|
||||
}
|
||||
jerr.Error.Body = string(slurp)
|
||||
jerr.Error.URL = res.Request.URL.String()
|
||||
return jerr.Error
|
||||
}
|
||||
}
|
||||
return &Error{
|
||||
StatusCode: res.StatusCode,
|
||||
Body: string(slurp),
|
||||
Header: res.Header,
|
||||
}
|
||||
}
|
||||
43
providers/azure/client/api/types.go
Normal file
43
providers/azure/client/api/types.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResponseMetadata is embedded in each response and contains the HTTP response code and headers from the server.
|
||||
type ResponseMetadata struct {
|
||||
// HTTPStatusCode is the server's response status code.
|
||||
HTTPStatusCode int
|
||||
// Header contains the response header fields from the server.
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
// JSONTime assumes the time format is RFC3339.
|
||||
type JSONTime time.Time
|
||||
|
||||
const AzureTimeFormat = "2006-01-02T15:04:05Z"
|
||||
|
||||
// MarshalJSON ensures that the time is serialized as RFC3339.
|
||||
func (t JSONTime) MarshalJSON() ([]byte, error) {
|
||||
// Serialize the JSON as RFC3339.
|
||||
stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format(AzureTimeFormat))
|
||||
return []byte(stamp), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON ensures that the time is deserialized as RFC3339.
|
||||
func (t *JSONTime) UnmarshalJSON(data []byte) error {
|
||||
if t == nil {
|
||||
return errors.New("api.JSONTime: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
|
||||
parsed, err := time.Parse(AzureTimeFormat, string(bytes.Trim(data, "\"")))
|
||||
if err != nil {
|
||||
return fmt.Errorf("api.JSONTime: UnmarshalJSON failed: %v", err)
|
||||
}
|
||||
*t = JSONTime(parsed)
|
||||
return nil
|
||||
}
|
||||
51
providers/azure/client/api/utils.go
Normal file
51
providers/azure/client/api/utils.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ResolveRelative combines a url base with a relative path.
|
||||
func ResolveRelative(basestr, relstr string) string {
|
||||
u, _ := url.Parse(basestr)
|
||||
rel, _ := url.Parse(relstr)
|
||||
u = u.ResolveReference(rel)
|
||||
us := u.String()
|
||||
us = strings.Replace(us, "%7B", "{", -1)
|
||||
us = strings.Replace(us, "%7D", "}", -1)
|
||||
return us
|
||||
}
|
||||
|
||||
// ExpandURL subsitutes any {{encoded}} strings in the URL passed in using
|
||||
// the map supplied.
|
||||
func ExpandURL(u *url.URL, expansions map[string]string) error {
|
||||
t, err := template.New("url").Parse(u.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Parsing template for url path %q failed: %v", u.Path, err)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := t.Execute(&b, expansions); err != nil {
|
||||
return fmt.Errorf("Executing template for url path failed: %v", err)
|
||||
}
|
||||
|
||||
// set the parameters
|
||||
u.Path = b.String()
|
||||
|
||||
// escape the expansions
|
||||
for k, v := range expansions {
|
||||
expansions[k] = url.QueryEscape(v)
|
||||
}
|
||||
|
||||
var bt bytes.Buffer
|
||||
if err := t.Execute(&bt, expansions); err != nil {
|
||||
return fmt.Errorf("Executing template for url path failed: %v", err)
|
||||
}
|
||||
|
||||
// set the parameters
|
||||
u.RawPath = bt.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
103
providers/azure/client/api/utils_test.go
Normal file
103
providers/azure/client/api/utils_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURI = "https://management.azure.com"
|
||||
)
|
||||
|
||||
type expandTest struct {
|
||||
in string
|
||||
expansions map[string]string
|
||||
want string
|
||||
}
|
||||
|
||||
var expandTests = []expandTest{
|
||||
// no expansions
|
||||
{
|
||||
"",
|
||||
map[string]string{},
|
||||
"https://management.azure.com",
|
||||
},
|
||||
// multiple expansions, no escaping
|
||||
{
|
||||
"subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups/{{.containerGroupName}}",
|
||||
map[string]string{
|
||||
"subscriptionId": "foo",
|
||||
"resourceGroup": "bar",
|
||||
"containerGroupName": "baz",
|
||||
},
|
||||
"https://management.azure.com/subscriptions/foo/resourceGroups/bar/providers/Microsoft.ContainerInstance/containerGroups/baz",
|
||||
},
|
||||
// one expansion, with hex escapes
|
||||
{
|
||||
"subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups/{{.containerGroupName}}",
|
||||
map[string]string{
|
||||
"subscriptionId": "foo/bar",
|
||||
"resourceGroup": "bar",
|
||||
"containerGroupName": "baz",
|
||||
},
|
||||
"https://management.azure.com/subscriptions/foo%2Fbar/resourceGroups/bar/providers/Microsoft.ContainerInstance/containerGroups/baz",
|
||||
},
|
||||
// one expansion, with space
|
||||
{
|
||||
"subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups/{{.containerGroupName}}",
|
||||
map[string]string{
|
||||
"subscriptionId": "foo and bar",
|
||||
"resourceGroup": "bar",
|
||||
"containerGroupName": "baz",
|
||||
},
|
||||
"https://management.azure.com/subscriptions/foo%20and%20bar/resourceGroups/bar/providers/Microsoft.ContainerInstance/containerGroups/baz",
|
||||
},
|
||||
// expansion not found
|
||||
{
|
||||
"subscriptions/{{.subscriptionId}}/resourceGroups/{{.resourceGroup}}/providers/Microsoft.ContainerInstance/containerGroups/{{.containerGroupName}}",
|
||||
map[string]string{
|
||||
"subscriptionId": "foo",
|
||||
"containerGroupName": "baz",
|
||||
},
|
||||
"https://management.azure.com/subscriptions/foo/resourceGroups/%3Cno%20value%3E/providers/Microsoft.ContainerInstance/containerGroups/baz",
|
||||
},
|
||||
// utf-8 characters
|
||||
{
|
||||
"{{.bucket}}/get",
|
||||
map[string]string{
|
||||
"bucket": "£100",
|
||||
},
|
||||
"https://management.azure.com/%C2%A3100/get",
|
||||
},
|
||||
// punctuations
|
||||
{
|
||||
"{{.bucket}}/get",
|
||||
map[string]string{
|
||||
"bucket": `/\@:,.`,
|
||||
},
|
||||
"https://management.azure.com/%2F%5C%40%3A%2C./get",
|
||||
},
|
||||
// mis-matched brackets
|
||||
{
|
||||
"/{{.bucket/get",
|
||||
map[string]string{
|
||||
"bucket": "red",
|
||||
},
|
||||
"https://management.azure.com/%7B%7B.bucket/get",
|
||||
},
|
||||
}
|
||||
|
||||
func TestExpandURL(t *testing.T) {
|
||||
for i, test := range expandTests {
|
||||
uri := ResolveRelative(baseURI, test.in)
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing url %q failed: %v", test.in, err)
|
||||
}
|
||||
ExpandURL(u, test.expansions)
|
||||
got := u.String()
|
||||
if got != test.want {
|
||||
t.Errorf("got %q expected %q in test %d", got, test.want, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
95
providers/azure/client/auth.go
Normal file
95
providers/azure/client/auth.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/dimchansky/utfbom"
|
||||
)
|
||||
|
||||
const (
|
||||
// AuthenticationFilepathName defines the name of the environment variable
|
||||
// containing the path to the file to be used to populate Authentication
|
||||
// for Azure.
|
||||
AuthenticationFilepathName = "AZURE_AUTH_LOCATION"
|
||||
)
|
||||
|
||||
// Authentication represents the authentication file for Azure.
|
||||
type Authentication struct {
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpointUrl,omitempty"`
|
||||
GraphResourceID string `json:"activeDirectoryGraphResourceId,omitempty"`
|
||||
SQLManagementEndpoint string `json:"sqlManagementEndpointUrl,omitempty"`
|
||||
GalleryEndpoint string `json:"galleryEndpointUrl,omitempty"`
|
||||
ManagementEndpoint string `json:"managementEndpointUrl,omitempty"`
|
||||
}
|
||||
|
||||
// NewAuthentication returns an authentication struct from user provided
|
||||
// credentials.
|
||||
func NewAuthentication(clientID, clientSecret, subscriptionID, tenantID string) *Authentication {
|
||||
return &Authentication{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
SubscriptionID: subscriptionID,
|
||||
TenantID: tenantID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAuthenticationFromFile returns an authentication struct from file located
|
||||
// at AZURE_AUTH_LOCATION.
|
||||
func NewAuthenticationFromFile() (*Authentication, error) {
|
||||
file := os.Getenv(AuthenticationFilepathName)
|
||||
if file == "" {
|
||||
return nil, fmt.Errorf("Authentication file not found, environment variable %s is not set", AuthenticationFilepathName)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Reading authentication file %q failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Authentication file might be encoded.
|
||||
decoded, err := decode(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Decoding authentication file %q failed: %v", file, err)
|
||||
}
|
||||
|
||||
// Unmarshal the authentication file.
|
||||
var auth Authentication
|
||||
if err := json.Unmarshal(decoded, &auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth, nil
|
||||
|
||||
}
|
||||
|
||||
func decode(b []byte) ([]byte, error) {
|
||||
reader, enc := utfbom.Skip(bytes.NewReader(b))
|
||||
|
||||
switch enc {
|
||||
case utfbom.UTF16LittleEndian:
|
||||
u16 := make([]uint16, (len(b)/2)-1)
|
||||
err := binary.Read(reader, binary.LittleEndian, &u16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16))), nil
|
||||
case utfbom.UTF16BigEndian:
|
||||
u16 := make([]uint16, (len(b)/2)-1)
|
||||
err := binary.Read(reader, binary.BigEndian, &u16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16))), nil
|
||||
}
|
||||
return ioutil.ReadAll(reader)
|
||||
}
|
||||
121
providers/azure/client/client.go
Normal file
121
providers/azure/client/client.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
)
|
||||
|
||||
// Client represents authentication details and cloud specific parameters for
|
||||
// Azure Resource Manager clients.
|
||||
type Client struct {
|
||||
Authentication *Authentication
|
||||
BaseURI string
|
||||
HTTPClient *http.Client
|
||||
BearerAuthorizer *BearerAuthorizer
|
||||
}
|
||||
|
||||
// BearerAuthorizer implements the bearer authorization.
|
||||
type BearerAuthorizer struct {
|
||||
tokenProvider adal.OAuthTokenProvider
|
||||
}
|
||||
|
||||
type userAgentTransport struct {
|
||||
userAgent string
|
||||
base http.RoundTripper
|
||||
client *Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Azure API client from an Authentication struct and BaseURI.
|
||||
func NewClient(auth *Authentication, baseURI string, userAgent string) (*Client, error) {
|
||||
resource, err := getResourceForToken(auth, baseURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Getting resource for token failed: %v", err)
|
||||
}
|
||||
client := &Client{
|
||||
Authentication: auth,
|
||||
BaseURI: resource,
|
||||
}
|
||||
|
||||
config, err := adal.NewOAuthConfig(auth.ActiveDirectoryEndpoint, auth.TenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating new OAuth config for active directory failed: %v", err)
|
||||
}
|
||||
|
||||
tp, err := adal.NewServicePrincipalToken(*config, auth.ClientID, auth.ClientSecret, resource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating new service principal token failed: %v", err)
|
||||
}
|
||||
|
||||
client.BearerAuthorizer = &BearerAuthorizer{tokenProvider: tp}
|
||||
|
||||
uat := userAgentTransport{
|
||||
base: http.DefaultTransport,
|
||||
userAgent: userAgent,
|
||||
client: client,
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: uat,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.base == nil {
|
||||
return nil, errors.New("RoundTrip: no Transport specified")
|
||||
}
|
||||
|
||||
newReq := *req
|
||||
newReq.Header = make(http.Header)
|
||||
for k, vv := range req.Header {
|
||||
newReq.Header[k] = vv
|
||||
}
|
||||
|
||||
// Add the user agent header.
|
||||
newReq.Header["User-Agent"] = []string{t.userAgent}
|
||||
|
||||
// Add the content-type header.
|
||||
newReq.Header["Content-Type"] = []string{"application/json"}
|
||||
|
||||
// Refresh the token if necessary
|
||||
// TODO: don't refresh the token everytime
|
||||
refresher, ok := t.client.BearerAuthorizer.tokenProvider.(adal.Refresher)
|
||||
if ok {
|
||||
if err := refresher.EnsureFresh(); err != nil {
|
||||
return nil, fmt.Errorf("Failed to refresh the authorization token for request to %s: %v", newReq.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the authorization header.
|
||||
newReq.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", t.client.BearerAuthorizer.tokenProvider.OAuthToken())}
|
||||
|
||||
return t.base.RoundTrip(&newReq)
|
||||
}
|
||||
|
||||
func getResourceForToken(auth *Authentication, baseURI string) (string, error) {
|
||||
// Compare dafault base URI from the SDK to the endpoints from the public cloud
|
||||
// Base URI and token resource are the same string. This func finds the authentication
|
||||
// file field that matches the SDK base URI. The SDK defines the public cloud
|
||||
// endpoint as its default base URI
|
||||
if !strings.HasSuffix(baseURI, "/") {
|
||||
baseURI += "/"
|
||||
}
|
||||
switch baseURI {
|
||||
case PublicCloud.ServiceManagementEndpoint:
|
||||
return auth.ManagementEndpoint, nil
|
||||
case PublicCloud.ResourceManagerEndpoint:
|
||||
return auth.ResourceManagerEndpoint, nil
|
||||
case PublicCloud.ActiveDirectoryEndpoint:
|
||||
return auth.ActiveDirectoryEndpoint, nil
|
||||
case PublicCloud.GalleryEndpoint:
|
||||
return auth.GalleryEndpoint, nil
|
||||
case PublicCloud.GraphEndpoint:
|
||||
return auth.GraphResourceID, nil
|
||||
}
|
||||
return "", fmt.Errorf("baseURI provided %q not found in endpoints", baseURI)
|
||||
}
|
||||
3
providers/azure/client/doc.go
Normal file
3
providers/azure/client/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package azure and subpackages are used to perform operations using the
|
||||
// Azure Resource Manager (ARM).
|
||||
package azure
|
||||
114
providers/azure/client/environments.go
Normal file
114
providers/azure/client/environments.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package azure
|
||||
|
||||
const (
|
||||
// EnvironmentFilepathName defines the name of the environment variable
|
||||
// containing the path to the file to be used to populate the Azure Environment.
|
||||
EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
|
||||
)
|
||||
|
||||
// Environment represents a set of endpoints for each of Azure's Clouds.
|
||||
type Environment struct {
|
||||
Name string `json:"name"`
|
||||
ManagementPortalURL string `json:"managementPortalURL"`
|
||||
PublishSettingsURL string `json:"publishSettingsURL"`
|
||||
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
||||
GalleryEndpoint string `json:"galleryEndpoint"`
|
||||
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
|
||||
GraphEndpoint string `json:"graphEndpoint"`
|
||||
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
|
||||
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
|
||||
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
|
||||
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
|
||||
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
|
||||
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
|
||||
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
|
||||
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
|
||||
}
|
||||
|
||||
var (
|
||||
// PublicCloud is the default public Azure cloud environment.
|
||||
PublicCloud = Environment{
|
||||
Name: "AzurePublicCloud",
|
||||
ManagementPortalURL: "https://manage.windowsazure.com/",
|
||||
PublishSettingsURL: "https://manage.windowsazure.com/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.windows.net/",
|
||||
ResourceManagerEndpoint: "https://management.azure.com/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
||||
GalleryEndpoint: "https://gallery.azure.com/",
|
||||
KeyVaultEndpoint: "https://vault.azure.net/",
|
||||
GraphEndpoint: "https://graph.windows.net/",
|
||||
StorageEndpointSuffix: "core.windows.net",
|
||||
SQLDatabaseDNSSuffix: "database.windows.net",
|
||||
TrafficManagerDNSSuffix: "trafficmanager.net",
|
||||
KeyVaultDNSSuffix: "vault.azure.net",
|
||||
ServiceBusEndpointSuffix: "servicebus.azure.com",
|
||||
ServiceManagementVMDNSSuffix: "cloudapp.net",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// USGovernmentCloud is the cloud environment for the US Government.
|
||||
USGovernmentCloud = Environment{
|
||||
Name: "AzureUSGovernmentCloud",
|
||||
ManagementPortalURL: "https://manage.windowsazure.us/",
|
||||
PublishSettingsURL: "https://manage.windowsazure.us/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.usgovcloudapi.net/",
|
||||
ResourceManagerEndpoint: "https://management.usgovcloudapi.net/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
||||
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
|
||||
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
|
||||
GraphEndpoint: "https://graph.usgovcloudapi.net/",
|
||||
StorageEndpointSuffix: "core.usgovcloudapi.net",
|
||||
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
|
||||
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
|
||||
KeyVaultDNSSuffix: "vault.usgovcloudapi.net",
|
||||
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
|
||||
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// ChinaCloud is the cloud environment operated in China.
|
||||
ChinaCloud = Environment{
|
||||
Name: "AzureChinaCloud",
|
||||
ManagementPortalURL: "https://manage.chinacloudapi.com/",
|
||||
PublishSettingsURL: "https://manage.chinacloudapi.com/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.chinacloudapi.cn/",
|
||||
ResourceManagerEndpoint: "https://management.chinacloudapi.cn/",
|
||||
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn/",
|
||||
GalleryEndpoint: "https://gallery.chinacloudapi.cn/",
|
||||
KeyVaultEndpoint: "https://vault.azure.cn/",
|
||||
GraphEndpoint: "https://graph.chinacloudapi.cn/",
|
||||
StorageEndpointSuffix: "core.chinacloudapi.cn",
|
||||
SQLDatabaseDNSSuffix: "database.chinacloudapi.cn",
|
||||
TrafficManagerDNSSuffix: "trafficmanager.cn",
|
||||
KeyVaultDNSSuffix: "vault.azure.cn",
|
||||
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.net",
|
||||
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// GermanCloud is the cloud environment operated in Germany.
|
||||
GermanCloud = Environment{
|
||||
Name: "AzureGermanCloud",
|
||||
ManagementPortalURL: "http://portal.microsoftazure.de/",
|
||||
PublishSettingsURL: "https://manage.microsoftazure.de/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.cloudapi.de/",
|
||||
ResourceManagerEndpoint: "https://management.microsoftazure.de/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.de/",
|
||||
GalleryEndpoint: "https://gallery.cloudapi.de/",
|
||||
KeyVaultEndpoint: "https://vault.microsoftazure.de/",
|
||||
GraphEndpoint: "https://graph.cloudapi.de/",
|
||||
StorageEndpointSuffix: "core.cloudapi.de",
|
||||
SQLDatabaseDNSSuffix: "database.cloudapi.de",
|
||||
TrafficManagerDNSSuffix: "azuretrafficmanager.de",
|
||||
KeyVaultDNSSuffix: "vault.microsoftazure.de",
|
||||
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
|
||||
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
)
|
||||
42
providers/azure/client/resourcegroups/client.go
Normal file
42
providers/azure/client/resourcegroups/client.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
azure "github.com/virtual-kubelet/virtual-kubelet/providers/azure/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// BaseURI is the default URI used for compute services.
|
||||
BaseURI = "https://management.azure.com"
|
||||
userAgent = "virtual-kubelet/azure-arm-resourcegroups/2017-12-01"
|
||||
apiVersion = "2017-08-01"
|
||||
|
||||
resourceGroupURLPath = "subscriptions/{{.subscriptionId}}/resourcegroups/{{.resourceGroupName}}"
|
||||
)
|
||||
|
||||
// Client is a client for interacting with Azure resource groups.
|
||||
//
|
||||
// Clients should be reused instead of created as needed.
|
||||
// The methods of Client are safe for concurrent use by multiple goroutines.
|
||||
type Client struct {
|
||||
hc *http.Client
|
||||
auth *azure.Authentication
|
||||
}
|
||||
|
||||
// NewClient creates a new Azure resource groups client.
|
||||
func NewClient() (*Client, error) {
|
||||
// Get authentication credentials from file.
|
||||
auth, err := azure.NewAuthenticationFromFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating azure authentication from file failed: %v", err)
|
||||
}
|
||||
|
||||
client, err := azure.NewClient(auth, BaseURI, userAgent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating azure client failed: %v", err)
|
||||
}
|
||||
|
||||
return &Client{hc: client.HTTPClient, auth: auth}, nil
|
||||
}
|
||||
99
providers/azure/client/resourcegroups/client_test.go
Normal file
99
providers/azure/client/resourcegroups/client_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
client *Client
|
||||
location = "eastus"
|
||||
resourceGroup = "virtual-kubelet-tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check if the AZURE_AUTH_LOCATION variable is already set.
|
||||
// If it is not set, set it to the root of this project in a credentials.json file.
|
||||
if os.Getenv("AZURE_AUTH_LOCATION") == "" {
|
||||
// Check if the credentials.json file exists in the root of this project.
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
dir := filepath.Dir(filename)
|
||||
file := filepath.Join(dir, "../../../../credentials.json")
|
||||
|
||||
// Check if the file exists.
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
log.Fatalf("Either set AZURE_AUTH_LOCATION or add a credentials.json file to the root of this project.")
|
||||
}
|
||||
|
||||
// Set the environment variable for the authentication file.
|
||||
os.Setenv("AZURE_AUTH_LOCATION", file)
|
||||
}
|
||||
|
||||
// Create a resource group name with uuid.
|
||||
uid := uuid.New()
|
||||
resourceGroup += "-" + uid.String()[0:6]
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
var err error
|
||||
client, err = NewClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupDoesNotExist(t *testing.T) {
|
||||
exists, err := client.ResourceGroupExists(resourceGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatal("resource group should not exist before it has been created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateResourceGroup(t *testing.T) {
|
||||
g, err := client.CreateResourceGroup(resourceGroup, Group{
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// check the name is the same
|
||||
if g.Name != resourceGroup {
|
||||
t.Fatalf("resource group name is %s, expected virtual-kubelet-tests", g.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupExists(t *testing.T) {
|
||||
exists, err := client.ResourceGroupExists(resourceGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("resource group should exist after being created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResourceGroup(t *testing.T) {
|
||||
g, err := client.GetResourceGroup(resourceGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// check the name is the same
|
||||
if g.Name != resourceGroup {
|
||||
t.Fatalf("resource group name is %s, expected %s", g.Name, resourceGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteResourceGroup(t *testing.T) {
|
||||
err := client.DeleteResourceGroup(resourceGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
68
providers/azure/client/resourcegroups/create.go
Normal file
68
providers/azure/client/resourcegroups/create.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// CreateResourceGroup creates a new Azure resource group with the
|
||||
// provided properties.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/createorupdate
|
||||
func (c *Client) CreateResourceGroup(resourceGroup string, properties Group) (*Group, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, resourceGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the body for the request.
|
||||
b := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(b).Encode(properties); err != nil {
|
||||
return nil, fmt.Errorf("Encoding create resource group body request failed: %v", err)
|
||||
}
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("PUT", uri, b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating create/update resource group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroupName": resourceGroup,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending create resource group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) and 201 (Created) are a successful responses.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create resource group returned an empty body in the response")
|
||||
}
|
||||
var g Group
|
||||
if err := json.NewDecoder(resp.Body).Decode(&g); err != nil {
|
||||
return nil, fmt.Errorf("Decoding create resource group response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &g, nil
|
||||
}
|
||||
54
providers/azure/client/resourcegroups/delete.go
Normal file
54
providers/azure/client/resourcegroups/delete.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// DeleteResourceGroup deletes an Azure resource group.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/delete
|
||||
func (c *Client) DeleteResourceGroup(resourceGroup string) error {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, resourceGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("DELETE", uri, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Creating delete resource group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroupName": resourceGroup,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Sending delete resource group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) and 202 (Accepted) are successful responses.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 204 No Content means the specified resource group was not found.
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return fmt.Errorf("Resource group with name %q was not found", resourceGroup)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
3
providers/azure/client/resourcegroups/doc.go
Normal file
3
providers/azure/client/resourcegroups/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package resourcegroups provides tools for interacting with the
|
||||
// Azure Resource Manager resource groups API.
|
||||
package resourcegroups
|
||||
60
providers/azure/client/resourcegroups/exists.go
Normal file
60
providers/azure/client/resourcegroups/exists.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// ResourceGroupExists checks if an Azure resource group exists.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/checkexistence
|
||||
func (c *Client) ResourceGroupExists(resourceGroup string) (bool, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, resourceGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("HEAD", uri, nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Creating resource group exists uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroupName": resourceGroup,
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Sending resource group exists request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// A 404 response means it does not exit.
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 204 (NoContent) and 404 are successful responses.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// A 204 status means it exists.
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// A 404 status means it does not exist.
|
||||
return false, nil
|
||||
}
|
||||
60
providers/azure/client/resourcegroups/get.go
Normal file
60
providers/azure/client/resourcegroups/get.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package resourcegroups
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
)
|
||||
|
||||
// GetResourceGroup gets an Azure resource group.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/resources/ResourceGroups/Get
|
||||
func (c *Client) GetResourceGroup(resourceGroup string) (*Group, error) {
|
||||
urlParams := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
}
|
||||
|
||||
// Create the url.
|
||||
uri := api.ResolveRelative(BaseURI, resourceGroupURLPath)
|
||||
uri += "?" + url.Values(urlParams).Encode()
|
||||
|
||||
// Create the request.
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating get resource group uri request failed: %v", err)
|
||||
}
|
||||
|
||||
// Add the parameters to the url.
|
||||
if err := api.ExpandURL(req.URL, map[string]string{
|
||||
"subscriptionId": c.auth.SubscriptionID,
|
||||
"resourceGroupName": resourceGroup,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("Expanding URL with parameters failed: %v", err)
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sending get resource group request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 200 (OK) is a success response.
|
||||
if err := api.CheckResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the body from the response.
|
||||
if resp.Body == nil {
|
||||
return nil, errors.New("Create resource group returned an empty body in the response")
|
||||
}
|
||||
var g Group
|
||||
if err := json.NewDecoder(resp.Body).Decode(&g); err != nil {
|
||||
return nil, fmt.Errorf("Decoding get resource group response body failed: %v", err)
|
||||
}
|
||||
|
||||
return &g, nil
|
||||
}
|
||||
19
providers/azure/client/resourcegroups/types.go
Normal file
19
providers/azure/client/resourcegroups/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package resourcegroups
|
||||
|
||||
import "github.com/virtual-kubelet/virtual-kubelet/providers/azure/client/api"
|
||||
|
||||
// Group holds resource group information.
|
||||
type Group struct {
|
||||
api.ResponseMetadata `json:"-"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Properties *GroupProperties `json:"properties,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
ManagedBy string `json:"managedBy,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// GroupProperties deines the properties for an Azure resource group.
|
||||
type GroupProperties struct {
|
||||
ProvisioningState string `json:"provisioningState,omitempty"`
|
||||
}
|
||||
8
providers/azure/client/resourcegroups/update.go
Normal file
8
providers/azure/client/resourcegroups/update.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package resourcegroups
|
||||
|
||||
// UpdateResourceGroup updates an Azure resource group with the
|
||||
// provided properties.
|
||||
// From: https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/createorupdate
|
||||
func (c *Client) UpdateResourceGroup(resourceGroup string, properties Group) (*Group, error) {
|
||||
return c.CreateResourceGroup(resourceGroup, properties)
|
||||
}
|
||||
58
providers/azure/config.go
Normal file
58
providers/azure/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers"
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type providerConfig struct {
|
||||
ResourceGroup string
|
||||
Region string
|
||||
OperatingSystem string
|
||||
CPU string
|
||||
Memory string
|
||||
Pods string
|
||||
}
|
||||
|
||||
func (p *ACIProvider) loadConfig(r io.Reader) error {
|
||||
var config providerConfig
|
||||
if _, err := toml.DecodeReader(r, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
p.region = config.Region
|
||||
p.resourceGroup = config.ResourceGroup
|
||||
|
||||
// Default to 20 mcpu
|
||||
p.cpu = "20"
|
||||
if config.CPU != "" {
|
||||
p.cpu = config.CPU
|
||||
}
|
||||
// Default to 100Gi
|
||||
p.memory = "100Gi"
|
||||
if config.Memory != "" {
|
||||
p.memory = config.Memory
|
||||
}
|
||||
// Default to 20 pods
|
||||
p.pods = "20"
|
||||
if config.Pods != "" {
|
||||
p.pods = config.Pods
|
||||
}
|
||||
|
||||
// Default to Linux if the operating system was not defined in the config.
|
||||
if config.OperatingSystem == "" {
|
||||
config.OperatingSystem = providers.OperatingSystemLinux
|
||||
} else {
|
||||
// Validate operating system from config.
|
||||
ok, _ := providers.ValidOperatingSystems[config.OperatingSystem]
|
||||
if !ok {
|
||||
return fmt.Errorf("%q is not a valid operating system, try one of the following instead: %s", config.OperatingSystem, strings.Join(providers.ValidOperatingSystems.Names(), " | "))
|
||||
}
|
||||
}
|
||||
|
||||
p.operatingSystem = config.OperatingSystem
|
||||
return nil
|
||||
}
|
||||
94
providers/azure/config_test.go
Normal file
94
providers/azure/config_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const cfg = `
|
||||
Region = "westus"
|
||||
ResourceGroup = "virtual-kubeletrg"
|
||||
CPU = "100"
|
||||
Memory = "100Gi"
|
||||
Pods = "20"`
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
br := bytes.NewReader([]byte(cfg))
|
||||
var p ACIProvider
|
||||
err := p.loadConfig(br)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
wanted := "westus"
|
||||
if p.region != wanted {
|
||||
t.Errorf("Wanted %s, got %s.", wanted, p.region)
|
||||
}
|
||||
|
||||
wanted = "virtual-kubeletrg"
|
||||
if p.resourceGroup != wanted {
|
||||
t.Errorf("Wanted %s, got %s.", wanted, p.resourceGroup)
|
||||
}
|
||||
|
||||
wanted = "100"
|
||||
if p.cpu != wanted {
|
||||
t.Errorf("Wanted %s, got %s.", wanted, p.cpu)
|
||||
}
|
||||
|
||||
wanted = "100Gi"
|
||||
if p.memory != wanted {
|
||||
t.Errorf("Wanted %s, got %s.", wanted, p.memory)
|
||||
}
|
||||
|
||||
wanted = "20"
|
||||
if p.pods != wanted {
|
||||
t.Errorf("Wanted %s, got %s.", wanted, p.pods)
|
||||
}
|
||||
}
|
||||
|
||||
const cfgBad = `
|
||||
Region = "westus"
|
||||
ResourceGroup = "virtual-kubeletrg"
|
||||
OperatingSystem = "noop"`
|
||||
|
||||
func TestBadConfig(t *testing.T) {
|
||||
br := bytes.NewReader([]byte(cfgBad))
|
||||
var p ACIProvider
|
||||
err := p.loadConfig(br)
|
||||
if err == nil {
|
||||
t.Fatal("expected loadConfig to fail with bad operating system option")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "is not a valid operating system") {
|
||||
t.Fatalf("expected loadConfig to fail with 'is not a valid operating system' but got: %v", err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const defCfg = `
|
||||
Region = "westus"
|
||||
ResourceGroup = "virtual-kubeletrg"`
|
||||
|
||||
func TestDefaultedConfig(t *testing.T) {
|
||||
br := bytes.NewReader([]byte(defCfg))
|
||||
var p ACIProvider
|
||||
err := p.loadConfig(br)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Test that defaults work with no settings in config.
|
||||
wanted := "20"
|
||||
if p.cpu != wanted {
|
||||
t.Errorf("Wanted default %s, got %s.", wanted, p.cpu)
|
||||
}
|
||||
|
||||
wanted = "100Gi"
|
||||
if p.memory != wanted {
|
||||
t.Errorf("Wanted default %s, got %s.", wanted, p.memory)
|
||||
}
|
||||
|
||||
wanted = "20"
|
||||
if p.pods != wanted {
|
||||
t.Errorf("Wanted default %s, got %s.", wanted, p.pods)
|
||||
}
|
||||
}
|
||||
8
providers/azure/example.toml
Normal file
8
providers/azure/example.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
# example configuration file for ACI virtual-kubelet
|
||||
|
||||
Region = "westus"
|
||||
ResourceGroup = "virtual-kubeletrg"
|
||||
OperatingSystem = "Linux"
|
||||
CPU = 100
|
||||
Memory = 100Gi
|
||||
Pods = 50
|
||||
58
providers/hypersh/config.go
Normal file
58
providers/hypersh/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package hypersh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers"
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type providerConfig struct {
|
||||
Region string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
OperatingSystem string
|
||||
CPU string
|
||||
Memory string
|
||||
Pods string
|
||||
}
|
||||
|
||||
func (p *HyperProvider) loadConfig(r io.Reader) error {
|
||||
var config providerConfig
|
||||
if _, err := toml.DecodeReader(r, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
p.region = config.Region
|
||||
p.accessKey = config.AccessKey
|
||||
p.secretKey = config.SecretKey
|
||||
|
||||
// Default to 20 mcpu
|
||||
p.cpu = "20"
|
||||
if config.CPU != "" {
|
||||
p.cpu = config.CPU
|
||||
}
|
||||
// Default to 100Gi
|
||||
p.memory = "100Gi"
|
||||
if config.Memory != "" {
|
||||
p.memory = config.Memory
|
||||
}
|
||||
// Default to 20 pods
|
||||
p.pods = "20"
|
||||
if config.Pods != "" {
|
||||
p.pods = config.Pods
|
||||
}
|
||||
|
||||
// Default to Linux if the operating system was not defined in the config.
|
||||
if config.OperatingSystem == "" {
|
||||
config.OperatingSystem = providers.OperatingSystemLinux
|
||||
}
|
||||
|
||||
// Validate operating system from config.
|
||||
if config.OperatingSystem != providers.OperatingSystemLinux {
|
||||
return fmt.Errorf("%q is not a valid operating system, only %s is valid", config.OperatingSystem, providers.OperatingSystemLinux)
|
||||
}
|
||||
|
||||
p.operatingSystem = config.OperatingSystem
|
||||
return nil
|
||||
}
|
||||
8
providers/hypersh/example.toml
Normal file
8
providers/hypersh/example.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
# example configuration file for Hyper.sh virtual-kubelet
|
||||
|
||||
AccessKey = ""
|
||||
SecretKey = ""
|
||||
Region = "us-west-1"
|
||||
CPU = 100
|
||||
Memory = 100Gi
|
||||
Pods = 50
|
||||
282
providers/hypersh/hypersh.go
Normal file
282
providers/hypersh/hypersh.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package hypersh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/docker/go-connections/nat"
|
||||
hyper "github.com/hyperhq/hyper-api/client"
|
||||
"github.com/hyperhq/hyper-api/types"
|
||||
"github.com/hyperhq/hyper-api/types/container"
|
||||
"github.com/hyperhq/hyper-api/types/filters"
|
||||
"github.com/hyperhq/hyper-api/types/network"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/manager"
|
||||
"github.com/virtual-kubelet/virtual-kubelet/providers"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
host = "https://us-west-1.hyper.sh"
|
||||
verStr = "v1.23"
|
||||
containerLabel = "hyper-virtual-kubelet"
|
||||
nodeLabel = containerLabel + "-node"
|
||||
)
|
||||
|
||||
// HyperProvider implements the virtual-kubelet provider interface and communicates with hyper.sh APIs.
|
||||
type HyperProvider struct {
|
||||
hyperClient *hyper.Client
|
||||
resourceManager *manager.ResourceManager
|
||||
nodeName string
|
||||
operatingSystem string
|
||||
region string
|
||||
accessKey string
|
||||
secretKey string
|
||||
cpu string
|
||||
memory string
|
||||
pods string
|
||||
}
|
||||
|
||||
// NewHyperProvider creates a new HyperProvider
|
||||
func NewHyperProvider(config string, rm *manager.ResourceManager, nodeName, operatingSystem string) (*HyperProvider, error) {
|
||||
var p HyperProvider
|
||||
var err error
|
||||
|
||||
p.resourceManager = rm
|
||||
|
||||
if config != "" {
|
||||
f, err := os.Open(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := p.loadConfig(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ak := os.Getenv("HYPERSH_ACCESS_KEY"); ak != "" {
|
||||
p.accessKey = ak
|
||||
}
|
||||
|
||||
if sk := os.Getenv("HYPERSH_SECRET_KEY"); sk != "" {
|
||||
p.secretKey = sk
|
||||
}
|
||||
|
||||
if r := os.Getenv("HYPERSH_REGION"); r != "" {
|
||||
p.region = r
|
||||
}
|
||||
|
||||
p.operatingSystem = operatingSystem
|
||||
p.nodeName = nodeName
|
||||
|
||||
p.hyperClient, err = hyper.NewClient(host, verStr, http.DefaultClient, nil, p.accessKey, p.secretKey, p.region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// CreatePod accepts a Pod definition and creates
|
||||
// a hyper.sh deployment
|
||||
func (p *HyperProvider) CreatePod(pod *v1.Pod) error {
|
||||
|
||||
// get containers
|
||||
containers, hostConfigs, err := getContainers(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: get registry creds
|
||||
// TODO: get volumes
|
||||
|
||||
// Iterate over the containers to create and start them.
|
||||
for k, ctr := range containers {
|
||||
containerName := fmt.Sprintf("pod-%s-%s", pod.Name, pod.Spec.Containers[k].Name)
|
||||
// Add labels to the pod containers.
|
||||
ctr.Labels = map[string]string{
|
||||
containerLabel: pod.Name,
|
||||
nodeLabel: p.nodeName,
|
||||
}
|
||||
|
||||
// Create the container.
|
||||
resp, err := p.hyperClient.ContainerCreate(context.Background(), &ctr, &hostConfigs[k], &network.NetworkingConfig{}, containerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Creating container %q failed in pod %q: %v", containerName, pod.Name, err)
|
||||
}
|
||||
// Iterate throught the warnings.
|
||||
for _, warning := range resp.Warnings {
|
||||
log.Printf("Warning while creating container %q for pod %q: %s", containerName, pod.Name, warning)
|
||||
}
|
||||
|
||||
// Start the container.
|
||||
if err := p.hyperClient.ContainerStart(context.Background(), resp.ID, ""); err != nil {
|
||||
return fmt.Errorf("Starting container %q failed in pod %q: %v", containerName, pod.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePod is a noop, hyper.sh currently does not support live updates of a pod.
|
||||
func (p *HyperProvider) UpdatePod(pod *v1.Pod) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePod deletes the specified pod out of hyper.sh.
|
||||
func (p *HyperProvider) DeletePod(pod *v1.Pod) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPod returns a pod by name that is running inside hyper.sh
|
||||
// returns nil if a pod by that name is not found.
|
||||
func (p *HyperProvider) GetPod(name string) (*v1.Pod, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetPodStatus returns the status of a pod by name that is running inside hyper.sh
|
||||
// returns nil if a pod by that name is not found.
|
||||
func (p *HyperProvider) GetPodStatus(name string) (*v1.PodStatus, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetPods returns a list of all pods known to be running within hyper.sh.
|
||||
func (p *HyperProvider) GetPods() ([]*v1.Pod, error) {
|
||||
filter, err := filters.FromParam(nodeLabel + "=" + p.nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating filter to get containers by node name failed: %v", err)
|
||||
}
|
||||
// Filter by label.
|
||||
_, err = p.hyperClient.ContainerList(context.Background(), types.ContainerListOptions{
|
||||
Filter: filter,
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Listing containers failed: %v", err)
|
||||
}
|
||||
|
||||
// TODO: convert containers into pods
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Capacity returns a resource list containing the capacity limits set for hyper.sh.
|
||||
func (p *HyperProvider) Capacity() v1.ResourceList {
|
||||
// TODO: These should be configurable
|
||||
return v1.ResourceList{
|
||||
"cpu": resource.MustParse("20"),
|
||||
"memory": resource.MustParse("100Gi"),
|
||||
"pods": resource.MustParse("20"),
|
||||
}
|
||||
}
|
||||
|
||||
// NodeConditions returns a list of conditions (Ready, OutOfDisk, etc), for updates to the node status
|
||||
// within Kuberentes.
|
||||
func (p *HyperProvider) NodeConditions() []v1.NodeCondition {
|
||||
// TODO: Make these dynamic and augment with custom hyper.sh specific conditions of interest
|
||||
return []v1.NodeCondition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: v1.ConditionTrue,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletReady",
|
||||
Message: "kubelet is ready.",
|
||||
},
|
||||
{
|
||||
Type: "OutOfDisk",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasSufficientDisk",
|
||||
Message: "kubelet has sufficient disk space available",
|
||||
},
|
||||
{
|
||||
Type: "MemoryPressure",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasSufficientMemory",
|
||||
Message: "kubelet has sufficient memory available",
|
||||
},
|
||||
{
|
||||
Type: "DiskPressure",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "KubeletHasNoDiskPressure",
|
||||
Message: "kubelet has no disk pressure",
|
||||
},
|
||||
{
|
||||
Type: "NetworkUnavailable",
|
||||
Status: v1.ConditionFalse,
|
||||
LastHeartbeatTime: metav1.Now(),
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: "RouteCreated",
|
||||
Message: "RouteController created a route",
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// OperatingSystem returns the operating system for this provider.
|
||||
// This is a noop to default to Linux for now.
|
||||
func (p *HyperProvider) OperatingSystem() string {
|
||||
return providers.OperatingSystemLinux
|
||||
}
|
||||
|
||||
func getContainers(pod *v1.Pod) ([]container.Config, []container.HostConfig, error) {
|
||||
containers := make([]container.Config, len(pod.Spec.Containers))
|
||||
hostConfigs := make([]container.HostConfig, len(pod.Spec.Containers))
|
||||
for x, ctr := range pod.Spec.Containers {
|
||||
// Do container.Config
|
||||
var c container.Config
|
||||
c.Image = ctr.Image
|
||||
c.Cmd = ctr.Command
|
||||
ports := map[nat.Port]struct{}{}
|
||||
portBindings := nat.PortMap{}
|
||||
for _, p := range ctr.Ports {
|
||||
port, err := nat.NewPort(string(p.Protocol), fmt.Sprintf("%d", p.HostPort))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating new port in container conversion failed: %v", err)
|
||||
}
|
||||
ports[port] = struct{}{}
|
||||
|
||||
portBindings[port] = []nat.PortBinding{
|
||||
{
|
||||
HostPort: fmt.Sprintf("%d", p.HostPort),
|
||||
},
|
||||
}
|
||||
}
|
||||
c.ExposedPorts = ports
|
||||
|
||||
// TODO: do volumes
|
||||
|
||||
envs := make([]string, len(ctr.Env))
|
||||
for z, e := range ctr.Env {
|
||||
envs[z] = fmt.Sprintf("%s=%s", e.Name, e.Value)
|
||||
}
|
||||
c.Env = envs
|
||||
|
||||
// Do container.HostConfig
|
||||
var hc container.HostConfig
|
||||
cpuLimit := ctr.Resources.Limits.Cpu().Value()
|
||||
memoryLimit := ctr.Resources.Limits.Memory().Value()
|
||||
|
||||
hc.Resources = container.Resources{
|
||||
CPUShares: cpuLimit,
|
||||
Memory: memoryLimit,
|
||||
}
|
||||
|
||||
hc.PortBindings = portBindings
|
||||
|
||||
containers[x] = c
|
||||
hostConfigs[x] = hc
|
||||
}
|
||||
return containers, hostConfigs, nil
|
||||
}
|
||||
27
providers/types.go
Normal file
27
providers/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package providers
|
||||
|
||||
const (
|
||||
// OperatingSystemLinux is the configuration value for defining Linux.
|
||||
OperatingSystemLinux = "Linux"
|
||||
// OperatingSystemWindows is the configuration value for defining Windows.
|
||||
OperatingSystemWindows = "Windows"
|
||||
)
|
||||
|
||||
type OperatingSystems map[string]bool
|
||||
|
||||
var (
|
||||
// ValidOperatingSystems defines the group of operating systems
|
||||
// that can be used as a kubelet node.
|
||||
ValidOperatingSystems = OperatingSystems{
|
||||
OperatingSystemLinux: true,
|
||||
OperatingSystemWindows: true,
|
||||
}
|
||||
)
|
||||
|
||||
func (o OperatingSystems) Names() []string {
|
||||
keys := make([]string, 0, len(o))
|
||||
for k := range o {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
27
scripts/create-connector.yaml
Normal file
27
scripts/create-connector.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: virtual-kubelet
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: virtual-kubelet
|
||||
spec:
|
||||
containers:
|
||||
- name: virtual-kubelet
|
||||
image: rbitia/virtual-kubelet:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: $clientId
|
||||
- name: AZURE_CLIENT_KEY
|
||||
value: $clientSecret
|
||||
- name: AZURE_TENANT_ID
|
||||
value: $tenantId
|
||||
- name: AZURE_SUBSCRIPTION_ID
|
||||
value: $subscriptionId
|
||||
- name: ACI_RESOURCE_GROUP
|
||||
value: $aciRecourceGroup
|
||||
16
scripts/createCredentials.sh
Normal file
16
scripts/createCredentials.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# This will build the credentials during the CI
|
||||
cat <<EOF > ${outputPathCredsfile}
|
||||
{
|
||||
"clientId": "$clientId",
|
||||
"clientSecret": "$clientSecret",
|
||||
"subscriptionId": "$subscriptionId",
|
||||
"tenantId": "$tenantId",
|
||||
"activeDirectoryEndpointUrl": "$activeDirectoryEndpointUrl",
|
||||
"resourceManagerEndpointUrl": "$resourceManagerEndpointUrl",
|
||||
"activeDirectoryGraphResourceId": "$activeDirectoryGraphResourceId",
|
||||
"sqlManagementEndpointUrl": "$sqlManagementEndpointUrl",
|
||||
"galleryEndpointUrl": "$galleryEndpointUrl",
|
||||
"managementEndpointUrl": "$managementEndpointUrl"
|
||||
}
|
||||
EOF
|
||||
1
scripts/envCreation.sh
Normal file
1
scripts/envCreation.sh
Normal file
@@ -0,0 +1 @@
|
||||
#!/bin/bash
|
||||
4
scripts/getKubeConfig.sh
Normal file
4
scripts/getKubeConfig.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# Retrieve kubeconfig for CI
|
||||
az login --service-principal -u http://azure-cli-2017-11-28-20-22-27 -p $clientSecret --tenant $tenantId
|
||||
az keyvault secret download --vault-name aciconnectorkv --name kubeconfig -f $PWD/config
|
||||
7
vendor/github.com/Azure/go-autorest/.github/PULL_REQUEST_TEMPLATE.md
generated
vendored
Normal file
7
vendor/github.com/Azure/go-autorest/.github/PULL_REQUEST_TEMPLATE.md
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
Thank you for your contribution to Go-AutoRest! We will triage and review it as soon as we can.
|
||||
|
||||
As part of submitting, please make sure you can make the following assertions:
|
||||
- [ ] I've tested my changes, adding unit tests if applicable.
|
||||
- [ ] I've added Apache 2.0 Headers to the top of any new source files.
|
||||
- [ ] I'm submitting this PR to the `dev` branch, except in the case of urgent bug fixes warranting their own release.
|
||||
- [ ] If I'm targeting `master`, I've updated [CHANGELOG.md](https://github.com/Azure/go-autorest/blob/master/CHANGELOG.md) to address the changes I'm making.
|
||||
31
vendor/github.com/Azure/go-autorest/.gitignore
generated
vendored
Normal file
31
vendor/github.com/Azure/go-autorest/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# The standard Go .gitignore file follows. (Sourced from: github.com/github/gitignore/master/Go.gitignore)
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
.DS_Store
|
||||
.idea/
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# go-autorest specific
|
||||
vendor/
|
||||
autorest/azure/example/example
|
||||
24
vendor/github.com/Azure/go-autorest/.travis.yml
generated
vendored
Normal file
24
vendor/github.com/Azure/go-autorest/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
sudo: false
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- 1.8
|
||||
- 1.7
|
||||
|
||||
install:
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- go get -u github.com/Masterminds/glide
|
||||
- go get -u github.com/stretchr/testify
|
||||
- go get -u github.com/GoASTScanner/gas
|
||||
- glide install
|
||||
|
||||
script:
|
||||
- grep -L -r --include *.go --exclude-dir vendor -P "Copyright (\d{4}|\(c\)) Microsoft" ./ | tee /dev/stderr | test -z "$(< /dev/stdin)"
|
||||
- test -z "$(gofmt -s -l -w ./autorest/. | tee /dev/stderr)"
|
||||
- test -z "$(golint ./autorest/... | tee /dev/stderr)"
|
||||
- go vet ./autorest/...
|
||||
- test -z "$(gas ./autorest/... | tee /dev/stderr | grep Error)"
|
||||
- go build -v ./autorest/...
|
||||
- go test -v ./autorest/...
|
||||
256
vendor/github.com/Azure/go-autorest/CHANGELOG.md
generated
vendored
Normal file
256
vendor/github.com/Azure/go-autorest/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,256 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v9.4.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update the AccessTokensPath() to read access tokens path through AZURE_ACCESS_TOKEN_FILE. If this
|
||||
environment variable is not set, it will fall back to use default path set by Azure CLI.
|
||||
- Use case-insensitive string comparison for polling states.
|
||||
|
||||
## v9.4.0
|
||||
|
||||
### New Features
|
||||
|
||||
- Added WaitForCompletion() to Future as a default polling implementation.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Method Future.Done() shouldn't update polling status for unexpected HTTP status codes.
|
||||
|
||||
## v9.3.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- DoRetryForStatusCodes will retry if sender.Do returns a non-nil error.
|
||||
|
||||
## v9.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- Added PollingMethod() to Future so callers know what kind of polling mechanism is used.
|
||||
- Added azure.ChangeToGet() which transforms an http.Request into a GET (to be used with LROs).
|
||||
|
||||
## v9.2.0
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for custom Azure Stack endpoints.
|
||||
- Added type azure.Future used to track the status of long-running operations.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Preserve the original error in DoRetryWithRegistration when registration fails.
|
||||
|
||||
## v9.1.1
|
||||
|
||||
- Fixes a bug regarding the cookie jar on `autorest.Client.Sender`.
|
||||
|
||||
## v9.1.0
|
||||
|
||||
### New Features
|
||||
|
||||
- In cases where there is a non-empty error from the service, attempt to unmarshal it instead of uniformly calling it an "Unknown" error.
|
||||
- Support for loading Azure CLI Authentication files.
|
||||
- Automatically register your subscription with the Azure Resource Provider if it hadn't been previously.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- RetriableRequest can now tolerate a ReadSeekable body being read but not reset.
|
||||
- Adding missing Apache Headers
|
||||
|
||||
## v9.0.0
|
||||
|
||||
> **IMPORTANT:** This release was intially labeled incorrectly as `v8.4.0`. From the time it was released, it should have been marked `v9.0.0` because it contains breaking changes to the MSI packages. We appologize for any inconvenience this causes.
|
||||
|
||||
Adding MSI Endpoint Support and CLI token rehydration.
|
||||
|
||||
## v8.3.1
|
||||
|
||||
Pick up bug fix in adal for MSI support.
|
||||
|
||||
## v8.3.0
|
||||
|
||||
Updates to Error string formats for clarity. Also, adding a copy of the http.Response to errors for an improved debugging experience.
|
||||
|
||||
## v8.2.0
|
||||
|
||||
### New Features
|
||||
|
||||
- Add support for bearer authentication callbacks
|
||||
- Support 429 response codes that include "Retry-After" header
|
||||
- Support validation constraint "Pattern" for map keys
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Make RetriableRequest work with multiple versions of Go
|
||||
|
||||
## v8.1.1
|
||||
Updates the RetriableRequest to take advantage of GetBody() added in Go 1.8.
|
||||
|
||||
## v8.1.0
|
||||
Adds RetriableRequest type for more efficient handling of retrying HTTP requests.
|
||||
|
||||
## v8.0.0
|
||||
|
||||
ADAL refactored into its own package.
|
||||
Support for UNIX time.
|
||||
|
||||
## v7.3.1
|
||||
- Version Testing now removed from production bits that are shipped with the library.
|
||||
|
||||
## v7.3.0
|
||||
- Exposing new `RespondDecorator`, `ByDiscardingBody`. This allows operations
|
||||
to acknowledge that they do not need either the entire or a trailing portion
|
||||
of accepts response body. In doing so, Go's http library can reuse HTTP
|
||||
connections more readily.
|
||||
- Adding `PrepareDecorator` to target custom BaseURLs.
|
||||
- Adding ACR suffix to public cloud environment.
|
||||
- Updating Glide dependencies.
|
||||
|
||||
## v7.2.5
|
||||
- Fixed the Active Directory endpoint for the China cloud.
|
||||
- Removes UTF-8 BOM if present in response payload.
|
||||
- Added telemetry.
|
||||
|
||||
## v7.2.3
|
||||
- Fixing bug in calls to `DelayForBackoff` that caused doubling of delay
|
||||
duration.
|
||||
|
||||
## v7.2.2
|
||||
- autorest/azure: added ASM and ARM VM DNS suffixes.
|
||||
|
||||
## v7.2.1
|
||||
- fixed parsing of UTC times that are not RFC3339 conformant.
|
||||
|
||||
## v7.2.0
|
||||
- autorest/validation: Reformat validation error for better error message.
|
||||
|
||||
## v7.1.0
|
||||
- preparer: Added support for multipart formdata - WithMultiPartFormdata()
|
||||
- preparer: Added support for sending file in request body - WithFile
|
||||
- client: Added RetryDuration parameter.
|
||||
- autorest/validation: new package for validation code for Azure Go SDK.
|
||||
|
||||
## v7.0.7
|
||||
- Add trailing / to endpoint
|
||||
- azure: add EnvironmentFromName
|
||||
|
||||
## v7.0.6
|
||||
- Add retry logic for 408, 500, 502, 503 and 504 status codes.
|
||||
- Change url path and query encoding logic.
|
||||
- Fix DelayForBackoff for proper exponential delay.
|
||||
- Add CookieJar in Client.
|
||||
|
||||
## v7.0.5
|
||||
- Add check to start polling only when status is in [200,201,202].
|
||||
- Refactoring for unchecked errors.
|
||||
- azure/persist changes.
|
||||
- Fix 'file in use' issue in renewing token in deviceflow.
|
||||
- Store header RetryAfter for subsequent requests in polling.
|
||||
- Add attribute details in service error.
|
||||
|
||||
## v7.0.4
|
||||
- Better error messages for long running operation failures
|
||||
|
||||
## v7.0.3
|
||||
- Corrected DoPollForAsynchronous to properly handle the initial response
|
||||
|
||||
## v7.0.2
|
||||
- Corrected DoPollForAsynchronous to continue using the polling method first discovered
|
||||
|
||||
## v7.0.1
|
||||
- Fixed empty JSON input error in ByUnmarshallingJSON
|
||||
- Fixed polling support for GET calls
|
||||
- Changed format name from TimeRfc1123 to TimeRFC1123
|
||||
|
||||
## v7.0.0
|
||||
- Added ByCopying responder with supporting TeeReadCloser
|
||||
- Rewrote Azure asynchronous handling
|
||||
- Reverted to only unmarshalling JSON
|
||||
- Corrected handling of RFC3339 time strings and added support for Rfc1123 time format
|
||||
|
||||
The `json.Decoder` does not catch bad data as thoroughly as `json.Unmarshal`. Since
|
||||
`encoding/json` successfully deserializes all core types, and extended types normally provide
|
||||
their custom JSON serialization handlers, the code has been reverted back to using
|
||||
`json.Unmarshal`. The original change to use `json.Decode` was made to reduce duplicate
|
||||
code; there is no loss of function, and there is a gain in accuracy, by reverting.
|
||||
|
||||
Additionally, Azure services indicate requests to be polled by multiple means. The existing code
|
||||
only checked for one of those (that is, the presence of the `Azure-AsyncOperation` header).
|
||||
The new code correctly covers all cases and aligns with the other Azure SDKs.
|
||||
|
||||
## v6.1.0
|
||||
- Introduced `date.ByUnmarshallingJSONDate` and `date.ByUnmarshallingJSONTime` to enable JSON encoded values.
|
||||
|
||||
## v6.0.0
|
||||
- Completely reworked the handling of polled and asynchronous requests
|
||||
- Removed unnecessary routines
|
||||
- Reworked `mocks.Sender` to replay a series of `http.Response` objects
|
||||
- Added `PrepareDecorators` for primitive types (e.g., bool, int32)
|
||||
|
||||
Handling polled and asynchronous requests is no longer part of `Client#Send`. Instead new
|
||||
`SendDecorators` implement different styles of polled behavior. See`autorest.DoPollForStatusCodes`
|
||||
and `azure.DoPollForAsynchronous` for examples.
|
||||
|
||||
## v5.0.0
|
||||
- Added new RespondDecorators unmarshalling primitive types
|
||||
- Corrected application of inspection and authorization PrependDecorators
|
||||
|
||||
## v4.0.0
|
||||
- Added support for Azure long-running operations.
|
||||
- Added cancelation support to all decorators and functions that may delay.
|
||||
- Breaking: `DelayForBackoff` now accepts a channel, which may be nil.
|
||||
|
||||
## v3.1.0
|
||||
- Add support for OAuth Device Flow authorization.
|
||||
- Add support for ServicePrincipalTokens that are backed by an existing token, rather than other secret material.
|
||||
- Add helpers for persisting and restoring Tokens.
|
||||
- Increased code coverage in the github.com/Azure/autorest/azure package
|
||||
|
||||
## v3.0.0
|
||||
- Breaking: `NewErrorWithError` no longer takes `statusCode int`.
|
||||
- Breaking: `NewErrorWithStatusCode` is replaced with `NewErrorWithResponse`.
|
||||
- Breaking: `Client#Send()` no longer takes `codes ...int` argument.
|
||||
- Add: XML unmarshaling support with `ByUnmarshallingXML()`
|
||||
- Stopped vending dependencies locally and switched to [Glide](https://github.com/Masterminds/glide).
|
||||
Applications using this library should either use Glide or vendor dependencies locally some other way.
|
||||
- Add: `azure.WithErrorUnlessStatusCode()` decorator to handle Azure errors.
|
||||
- Fix: use `net/http.DefaultClient` as base client.
|
||||
- Fix: Missing inspection for polling responses added.
|
||||
- Add: CopyAndDecode helpers.
|
||||
- Improved `./autorest/to` with `[]string` helpers.
|
||||
- Removed golint suppressions in .travis.yml.
|
||||
|
||||
## v2.1.0
|
||||
|
||||
- Added `StatusCode` to `Error` for more easily obtaining the HTTP Reponse StatusCode (if any)
|
||||
|
||||
## v2.0.0
|
||||
|
||||
- Changed `to.StringMapPtr` method signature to return a pointer
|
||||
- Changed `ServicePrincipalCertificateSecret` and `NewServicePrincipalTokenFromCertificate` to support generic certificate and private keys
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- Added Logging inspectors to trace http.Request / Response
|
||||
- Added support for User-Agent header
|
||||
- Changed WithHeader PrepareDecorator to use set vs. add
|
||||
- Added JSON to error when unmarshalling fails
|
||||
- Added Client#Send method
|
||||
- Corrected case of "Azure" in package paths
|
||||
- Added "to" helpers, Azure helpers, and improved ease-of-use
|
||||
- Corrected golint issues
|
||||
|
||||
## v1.0.1
|
||||
|
||||
- Added CHANGELOG.md
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- Added mechanism to retrieve a ServicePrincipalToken using a certificate-signed JWT
|
||||
- Added an example of creating a certificate-based ServicePrincipal and retrieving an OAuth token using the certificate
|
||||
|
||||
## v1.1.1
|
||||
|
||||
- Introduce godeps and vendor dependencies introduced in v1.1.1
|
||||
23
vendor/github.com/Azure/go-autorest/GNUmakefile
generated
vendored
Normal file
23
vendor/github.com/Azure/go-autorest/GNUmakefile
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
DIR?=./autorest/
|
||||
|
||||
default: build
|
||||
|
||||
build: fmt
|
||||
go install $(DIR)
|
||||
|
||||
test:
|
||||
go test $(DIR) || exit 1
|
||||
|
||||
vet:
|
||||
@echo "go vet ."
|
||||
@go vet $(DIR)... ; if [ $$? -eq 1 ]; then \
|
||||
echo ""; \
|
||||
echo "Vet found suspicious constructs. Please check the reported constructs"; \
|
||||
echo "and fix them if necessary before submitting the code for review."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
fmt:
|
||||
gofmt -w $(DIR)
|
||||
|
||||
.PHONY: build test vet fmt
|
||||
191
vendor/github.com/Azure/go-autorest/LICENSE
generated
vendored
Normal file
191
vendor/github.com/Azure/go-autorest/LICENSE
generated
vendored
Normal file
@@ -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 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
132
vendor/github.com/Azure/go-autorest/README.md
generated
vendored
Normal file
132
vendor/github.com/Azure/go-autorest/README.md
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
# go-autorest
|
||||
|
||||
[](https://godoc.org/github.com/Azure/go-autorest/autorest) [](https://travis-ci.org/Azure/go-autorest) [](https://goreportcard.com/report/Azure/go-autorest)
|
||||
|
||||
## Usage
|
||||
Package autorest implements an HTTP request pipeline suitable for use across multiple go-routines
|
||||
and provides the shared routines relied on by AutoRest (see https://github.com/Azure/autorest/)
|
||||
generated Go code.
|
||||
|
||||
The package breaks sending and responding to HTTP requests into three phases: Preparing, Sending,
|
||||
and Responding. A typical pattern is:
|
||||
|
||||
```go
|
||||
req, err := Prepare(&http.Request{},
|
||||
token.WithAuthorization())
|
||||
|
||||
resp, err := Send(req,
|
||||
WithLogging(logger),
|
||||
DoErrorIfStatusCode(http.StatusInternalServerError),
|
||||
DoCloseIfError(),
|
||||
DoRetryForAttempts(5, time.Second))
|
||||
|
||||
err = Respond(resp,
|
||||
ByDiscardingBody(),
|
||||
ByClosing())
|
||||
```
|
||||
|
||||
Each phase relies on decorators to modify and / or manage processing. Decorators may first modify
|
||||
and then pass the data along, pass the data first and then modify the result, or wrap themselves
|
||||
around passing the data (such as a logger might do). Decorators run in the order provided. For
|
||||
example, the following:
|
||||
|
||||
```go
|
||||
req, err := Prepare(&http.Request{},
|
||||
WithBaseURL("https://microsoft.com/"),
|
||||
WithPath("a"),
|
||||
WithPath("b"),
|
||||
WithPath("c"))
|
||||
```
|
||||
|
||||
will set the URL to:
|
||||
|
||||
```
|
||||
https://microsoft.com/a/b/c
|
||||
```
|
||||
|
||||
Preparers and Responders may be shared and re-used (assuming the underlying decorators support
|
||||
sharing and re-use). Performant use is obtained by creating one or more Preparers and Responders
|
||||
shared among multiple go-routines, and a single Sender shared among multiple sending go-routines,
|
||||
all bound together by means of input / output channels.
|
||||
|
||||
Decorators hold their passed state within a closure (such as the path components in the example
|
||||
above). Be careful to share Preparers and Responders only in a context where such held state
|
||||
applies. For example, it may not make sense to share a Preparer that applies a query string from a
|
||||
fixed set of values. Similarly, sharing a Responder that reads the response body into a passed
|
||||
struct (e.g., `ByUnmarshallingJson`) is likely incorrect.
|
||||
|
||||
Errors raised by autorest objects and methods will conform to the `autorest.Error` interface.
|
||||
|
||||
See the included examples for more detail. For details on the suggested use of this package by
|
||||
generated clients, see the Client described below.
|
||||
|
||||
## Helpers
|
||||
|
||||
### Handling Swagger Dates
|
||||
|
||||
The Swagger specification (https://swagger.io) that drives AutoRest
|
||||
(https://github.com/Azure/autorest/) precisely defines two date forms: date and date-time. The
|
||||
github.com/Azure/go-autorest/autorest/date package provides time.Time derivations to ensure correct
|
||||
parsing and formatting.
|
||||
|
||||
### Handling Empty Values
|
||||
|
||||
In JSON, missing values have different semantics than empty values. This is especially true for
|
||||
services using the HTTP PATCH verb. The JSON submitted with a PATCH request generally contains
|
||||
only those values to modify. Missing values are to be left unchanged. Developers, then, require a
|
||||
means to both specify an empty value and to leave the value out of the submitted JSON.
|
||||
|
||||
The Go JSON package (`encoding/json`) supports the `omitempty` tag. When specified, it omits
|
||||
empty values from the rendered JSON. Since Go defines default values for all base types (such as ""
|
||||
for string and 0 for int) and provides no means to mark a value as actually empty, the JSON package
|
||||
treats default values as meaning empty, omitting them from the rendered JSON. This means that, using
|
||||
the Go base types encoded through the default JSON package, it is not possible to create JSON to
|
||||
clear a value at the server.
|
||||
|
||||
The workaround within the Go community is to use pointers to base types in lieu of base types within
|
||||
structures that map to JSON. For example, instead of a value of type `string`, the workaround uses
|
||||
`*string`. While this enables distinguishing empty values from those to be unchanged, creating
|
||||
pointers to a base type (notably constant, in-line values) requires additional variables. This, for
|
||||
example,
|
||||
|
||||
```go
|
||||
s := struct {
|
||||
S *string
|
||||
}{ S: &"foo" }
|
||||
```
|
||||
fails, while, this
|
||||
|
||||
```go
|
||||
v := "foo"
|
||||
s := struct {
|
||||
S *string
|
||||
}{ S: &v }
|
||||
```
|
||||
succeeds.
|
||||
|
||||
To ease using pointers, the subpackage `to` contains helpers that convert to and from pointers for
|
||||
Go base types which have Swagger analogs. It also provides a helper that converts between
|
||||
`map[string]string` and `map[string]*string`, enabling the JSON to specify that the value
|
||||
associated with a key should be cleared. With the helpers, the previous example becomes
|
||||
|
||||
```go
|
||||
s := struct {
|
||||
S *string
|
||||
}{ S: to.StringPtr("foo") }
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/Azure/go-autorest/autorest
|
||||
go get github.com/Azure/go-autorest/autorest/azure
|
||||
go get github.com/Azure/go-autorest/autorest/date
|
||||
go get github.com/Azure/go-autorest/autorest/to
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file.
|
||||
|
||||
-----
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
253
vendor/github.com/Azure/go-autorest/autorest/adal/README.md
generated
vendored
Normal file
253
vendor/github.com/Azure/go-autorest/autorest/adal/README.md
generated
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
# Azure Active Directory library for Go
|
||||
|
||||
This project provides a stand alone Azure Active Directory library for Go. The code was extracted
|
||||
from [go-autorest](https://github.com/Azure/go-autorest/) project, which is used as a base for
|
||||
[azure-sdk-for-go](https://github.com/Azure/azure-sdk-for-go).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get -u github.com/Azure/go-autorest/autorest/adal
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
An Active Directory application is required in order to use this library. An application can be registered in the [Azure Portal](https://portal.azure.com/) follow these [guidelines](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications) or using the [Azure CLI](https://github.com/Azure/azure-cli).
|
||||
|
||||
### Register an Azure AD Application with secret
|
||||
|
||||
|
||||
1. Register a new application with a `secret` credential
|
||||
|
||||
```
|
||||
az ad app create \
|
||||
--display-name example-app \
|
||||
--homepage https://example-app/home \
|
||||
--identifier-uris https://example-app/app \
|
||||
--password secret
|
||||
```
|
||||
|
||||
2. Create a service principal using the `Application ID` from previous step
|
||||
|
||||
```
|
||||
az ad sp create --id "Application ID"
|
||||
```
|
||||
|
||||
* Replace `Application ID` with `appId` from step 1.
|
||||
|
||||
### Register an Azure AD Application with certificate
|
||||
|
||||
1. Create a private key
|
||||
|
||||
```
|
||||
openssl genrsa -out "example-app.key" 2048
|
||||
```
|
||||
|
||||
2. Create the certificate
|
||||
|
||||
```
|
||||
openssl req -new -key "example-app.key" -subj "/CN=example-app" -out "example-app.csr"
|
||||
openssl x509 -req -in "example-app.csr" -signkey "example-app.key" -out "example-app.crt" -days 10000
|
||||
```
|
||||
|
||||
3. Create the PKCS12 version of the certificate containing also the private key
|
||||
|
||||
```
|
||||
openssl pkcs12 -export -out "example-app.pfx" -inkey "example-app.key" -in "example-app.crt" -passout pass:
|
||||
|
||||
```
|
||||
|
||||
4. Register a new application with the certificate content form `example-app.crt`
|
||||
|
||||
```
|
||||
certificateContents="$(tail -n+2 "example-app.crt" | head -n-1)"
|
||||
|
||||
az ad app create \
|
||||
--display-name example-app \
|
||||
--homepage https://example-app/home \
|
||||
--identifier-uris https://example-app/app \
|
||||
--key-usage Verify --end-date 2018-01-01 \
|
||||
--key-value "${certificateContents}"
|
||||
```
|
||||
|
||||
5. Create a service principal using the `Application ID` from previous step
|
||||
|
||||
```
|
||||
az ad sp create --id "APPLICATION_ID"
|
||||
```
|
||||
|
||||
* Replace `APPLICATION_ID` with `appId` from step 4.
|
||||
|
||||
|
||||
### Grant the necessary permissions
|
||||
|
||||
Azure relies on a Role-Based Access Control (RBAC) model to manage the access to resources at a fine-grained
|
||||
level. There is a set of [pre-defined roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-built-in-roles)
|
||||
which can be assigned to a service principal of an Azure AD application depending of your needs.
|
||||
|
||||
```
|
||||
az role assignment create --assigner "SERVICE_PRINCIPAL_ID" --role "ROLE_NAME"
|
||||
```
|
||||
|
||||
* Replace the `SERVICE_PRINCIPAL_ID` with the `appId` from previous step.
|
||||
* Replace the `ROLE_NAME` with a role name of your choice.
|
||||
|
||||
It is also possible to define custom role definitions.
|
||||
|
||||
```
|
||||
az role definition create --role-definition role-definition.json
|
||||
```
|
||||
|
||||
* Check [custom roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-control-custom-roles) for more details regarding the content of `role-definition.json` file.
|
||||
|
||||
|
||||
### Acquire Access Token
|
||||
|
||||
The common configuration used by all flows:
|
||||
|
||||
```Go
|
||||
const activeDirectoryEndpoint = "https://login.microsoftonline.com/"
|
||||
tenantID := "TENANT_ID"
|
||||
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
|
||||
|
||||
applicationID := "APPLICATION_ID"
|
||||
|
||||
callback := func(token adal.Token) error {
|
||||
// This is called after the token is acquired
|
||||
}
|
||||
|
||||
// The resource for which the token is acquired
|
||||
resource := "https://management.core.windows.net/"
|
||||
```
|
||||
|
||||
* Replace the `TENANT_ID` with your tenant ID.
|
||||
* Replace the `APPLICATION_ID` with the value from previous section.
|
||||
|
||||
#### Client Credentials
|
||||
|
||||
```Go
|
||||
applicationSecret := "APPLICATION_SECRET"
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(
|
||||
oauthConfig,
|
||||
appliationID,
|
||||
applicationSecret,
|
||||
resource,
|
||||
callbacks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Acquire a new access token
|
||||
err = spt.Refresh()
|
||||
if (err == nil) {
|
||||
token := spt.Token
|
||||
}
|
||||
```
|
||||
|
||||
* Replace the `APPLICATION_SECRET` with the `password` value from previous section.
|
||||
|
||||
#### Client Certificate
|
||||
|
||||
```Go
|
||||
certificatePath := "./example-app.pfx"
|
||||
|
||||
certData, err := ioutil.ReadFile(certificatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", certificatePath, err)
|
||||
}
|
||||
|
||||
// Get the certificate and private key from pfx file
|
||||
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalTokenFromCertificate(
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
certificate,
|
||||
rsaPrivateKey,
|
||||
resource,
|
||||
callbacks...)
|
||||
|
||||
// Acquire a new access token
|
||||
err = spt.Refresh()
|
||||
if (err == nil) {
|
||||
token := spt.Token
|
||||
}
|
||||
```
|
||||
|
||||
* Update the certificate path to point to the example-app.pfx file which was created in previous section.
|
||||
|
||||
|
||||
#### Device Code
|
||||
|
||||
```Go
|
||||
oauthClient := &http.Client{}
|
||||
|
||||
// Acquire the device code
|
||||
deviceCode, err := adal.InitiateDeviceAuth(
|
||||
oauthClient,
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
resource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to start device auth flow: %s", err)
|
||||
}
|
||||
|
||||
// Display the authentication message
|
||||
fmt.Println(*deviceCode.Message)
|
||||
|
||||
// Wait here until the user is authenticated
|
||||
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to finish device auth flow: %s", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalTokenFromManualToken(
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
resource,
|
||||
*token,
|
||||
callbacks...)
|
||||
|
||||
if (err == nil) {
|
||||
token := spt.Token
|
||||
}
|
||||
```
|
||||
|
||||
### Command Line Tool
|
||||
|
||||
A command line tool is available in `cmd/adal.go` that can acquire a token for a given resource. It supports all flows mentioned above.
|
||||
|
||||
```
|
||||
adal -h
|
||||
|
||||
Usage of ./adal:
|
||||
-applicationId string
|
||||
application id
|
||||
-certificatePath string
|
||||
path to pk12/PFC application certificate
|
||||
-mode string
|
||||
authentication mode (device, secret, cert, refresh) (default "device")
|
||||
-resource string
|
||||
resource for which the token is requested
|
||||
-secret string
|
||||
application secret
|
||||
-tenantId string
|
||||
tenant id
|
||||
-tokenCachePath string
|
||||
location of oath token cache (default "/home/cgc/.adal/accessToken.json")
|
||||
```
|
||||
|
||||
Example acquire a token for `https://management.core.windows.net/` using device code flow:
|
||||
|
||||
```
|
||||
adal -mode device \
|
||||
-applicationId "APPLICATION_ID" \
|
||||
-tenantId "TENANT_ID" \
|
||||
-resource https://management.core.windows.net/
|
||||
|
||||
```
|
||||
298
vendor/github.com/Azure/go-autorest/autorest/adal/cmd/adal.go
generated
vendored
Normal file
298
vendor/github.com/Azure/go-autorest/autorest/adal/cmd/adal.go
generated
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/user"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
const (
|
||||
deviceMode = "device"
|
||||
clientSecretMode = "secret"
|
||||
clientCertMode = "cert"
|
||||
refreshMode = "refresh"
|
||||
|
||||
activeDirectoryEndpoint = "https://login.microsoftonline.com/"
|
||||
)
|
||||
|
||||
type option struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
var (
|
||||
mode string
|
||||
resource string
|
||||
|
||||
tenantID string
|
||||
applicationID string
|
||||
|
||||
applicationSecret string
|
||||
certificatePath string
|
||||
|
||||
tokenCachePath string
|
||||
)
|
||||
|
||||
func checkMandatoryOptions(mode string, options ...option) {
|
||||
for _, option := range options {
|
||||
if strings.TrimSpace(option.value) == "" {
|
||||
log.Fatalf("Authentication mode '%s' requires mandatory option '%s'.", mode, option.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTokenCachePath() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defaultTokenPath := usr.HomeDir + "/.adal/accessToken.json"
|
||||
return defaultTokenPath
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&mode, "mode", "device", "authentication mode (device, secret, cert, refresh)")
|
||||
flag.StringVar(&resource, "resource", "", "resource for which the token is requested")
|
||||
flag.StringVar(&tenantID, "tenantId", "", "tenant id")
|
||||
flag.StringVar(&applicationID, "applicationId", "", "application id")
|
||||
flag.StringVar(&applicationSecret, "secret", "", "application secret")
|
||||
flag.StringVar(&certificatePath, "certificatePath", "", "path to pk12/PFC application certificate")
|
||||
flag.StringVar(&tokenCachePath, "tokenCachePath", defaultTokenCachePath(), "location of oath token cache")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
switch mode = strings.TrimSpace(mode); mode {
|
||||
case clientSecretMode:
|
||||
checkMandatoryOptions(clientSecretMode,
|
||||
option{name: "resource", value: resource},
|
||||
option{name: "tenantId", value: tenantID},
|
||||
option{name: "applicationId", value: applicationID},
|
||||
option{name: "secret", value: applicationSecret},
|
||||
)
|
||||
case clientCertMode:
|
||||
checkMandatoryOptions(clientCertMode,
|
||||
option{name: "resource", value: resource},
|
||||
option{name: "tenantId", value: tenantID},
|
||||
option{name: "applicationId", value: applicationID},
|
||||
option{name: "certificatePath", value: certificatePath},
|
||||
)
|
||||
case deviceMode:
|
||||
checkMandatoryOptions(deviceMode,
|
||||
option{name: "resource", value: resource},
|
||||
option{name: "tenantId", value: tenantID},
|
||||
option{name: "applicationId", value: applicationID},
|
||||
)
|
||||
case refreshMode:
|
||||
checkMandatoryOptions(refreshMode,
|
||||
option{name: "resource", value: resource},
|
||||
option{name: "tenantId", value: tenantID},
|
||||
option{name: "applicationId", value: applicationID},
|
||||
)
|
||||
default:
|
||||
log.Fatalln("Authentication modes 'secret, 'cert', 'device' or 'refresh' are supported.")
|
||||
}
|
||||
}
|
||||
|
||||
func acquireTokenClientSecretFlow(oauthConfig adal.OAuthConfig,
|
||||
appliationID string,
|
||||
applicationSecret string,
|
||||
resource string,
|
||||
callbacks ...adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(
|
||||
oauthConfig,
|
||||
appliationID,
|
||||
applicationSecret,
|
||||
resource,
|
||||
callbacks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spt, spt.Refresh()
|
||||
}
|
||||
|
||||
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
|
||||
if !isRsaKey {
|
||||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
|
||||
}
|
||||
|
||||
return certificate, rsaPrivateKey, nil
|
||||
}
|
||||
|
||||
func acquireTokenClientCertFlow(oauthConfig adal.OAuthConfig,
|
||||
applicationID string,
|
||||
applicationCertPath string,
|
||||
resource string,
|
||||
callbacks ...adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
|
||||
|
||||
certData, err := ioutil.ReadFile(certificatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", certificatePath, err)
|
||||
}
|
||||
|
||||
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalTokenFromCertificate(
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
certificate,
|
||||
rsaPrivateKey,
|
||||
resource,
|
||||
callbacks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spt, spt.Refresh()
|
||||
}
|
||||
|
||||
func acquireTokenDeviceCodeFlow(oauthConfig adal.OAuthConfig,
|
||||
applicationID string,
|
||||
resource string,
|
||||
callbacks ...adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
|
||||
|
||||
oauthClient := &http.Client{}
|
||||
deviceCode, err := adal.InitiateDeviceAuth(
|
||||
oauthClient,
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
resource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to start device auth flow: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(*deviceCode.Message)
|
||||
|
||||
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to finish device auth flow: %s", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalTokenFromManualToken(
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
resource,
|
||||
*token,
|
||||
callbacks...)
|
||||
return spt, err
|
||||
}
|
||||
|
||||
func refreshToken(oauthConfig adal.OAuthConfig,
|
||||
applicationID string,
|
||||
resource string,
|
||||
tokenCachePath string,
|
||||
callbacks ...adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
|
||||
|
||||
token, err := adal.LoadToken(tokenCachePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load token from cache: %v", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalTokenFromManualToken(
|
||||
oauthConfig,
|
||||
applicationID,
|
||||
resource,
|
||||
*token,
|
||||
callbacks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spt, spt.Refresh()
|
||||
}
|
||||
|
||||
func saveToken(spt adal.Token) error {
|
||||
if tokenCachePath != "" {
|
||||
err := adal.SaveToken(tokenCachePath, 0600, spt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Acquired token was saved in '%s' file\n", tokenCachePath)
|
||||
return nil
|
||||
|
||||
}
|
||||
return fmt.Errorf("empty path for token cache")
|
||||
}
|
||||
|
||||
func main() {
|
||||
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
callback := func(token adal.Token) error {
|
||||
return saveToken(token)
|
||||
}
|
||||
|
||||
log.Printf("Authenticating with mode '%s'\n", mode)
|
||||
switch mode {
|
||||
case clientSecretMode:
|
||||
_, err = acquireTokenClientSecretFlow(
|
||||
*oauthConfig,
|
||||
applicationID,
|
||||
applicationSecret,
|
||||
resource,
|
||||
callback)
|
||||
case clientCertMode:
|
||||
_, err = acquireTokenClientCertFlow(
|
||||
*oauthConfig,
|
||||
applicationID,
|
||||
certificatePath,
|
||||
resource,
|
||||
callback)
|
||||
case deviceMode:
|
||||
var spt *adal.ServicePrincipalToken
|
||||
spt, err = acquireTokenDeviceCodeFlow(
|
||||
*oauthConfig,
|
||||
applicationID,
|
||||
resource,
|
||||
callback)
|
||||
if err == nil {
|
||||
err = saveToken(spt.Token)
|
||||
}
|
||||
case refreshMode:
|
||||
_, err = refreshToken(
|
||||
*oauthConfig,
|
||||
applicationID,
|
||||
resource,
|
||||
tokenCachePath,
|
||||
callback)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to acquire a token for resource %s. Error: %v", resource, err)
|
||||
}
|
||||
}
|
||||
65
vendor/github.com/Azure/go-autorest/autorest/adal/config.go
generated
vendored
Normal file
65
vendor/github.com/Azure/go-autorest/autorest/adal/config.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
activeDirectoryAPIVersion = "1.0"
|
||||
)
|
||||
|
||||
// OAuthConfig represents the endpoints needed
|
||||
// in OAuth operations
|
||||
type OAuthConfig struct {
|
||||
AuthorityEndpoint url.URL
|
||||
AuthorizeEndpoint url.URL
|
||||
TokenEndpoint url.URL
|
||||
DeviceCodeEndpoint url.URL
|
||||
}
|
||||
|
||||
// NewOAuthConfig returns an OAuthConfig with tenant specific urls
|
||||
func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, error) {
|
||||
const activeDirectoryEndpointTemplate = "%s/oauth2/%s?api-version=%s"
|
||||
u, err := url.Parse(activeDirectoryEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorityURL, err := u.Parse(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "authorize", activeDirectoryAPIVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "token", activeDirectoryAPIVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceCodeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "devicecode", activeDirectoryAPIVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OAuthConfig{
|
||||
AuthorityEndpoint: *authorityURL,
|
||||
AuthorizeEndpoint: *authorizeURL,
|
||||
TokenEndpoint: *tokenURL,
|
||||
DeviceCodeEndpoint: *deviceCodeURL,
|
||||
}, nil
|
||||
}
|
||||
44
vendor/github.com/Azure/go-autorest/autorest/adal/config_test.go
generated
vendored
Normal file
44
vendor/github.com/Azure/go-autorest/autorest/adal/config_test.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewOAuthConfig(t *testing.T) {
|
||||
const testActiveDirectoryEndpoint = "https://login.test.com"
|
||||
const testTenantID = "tenant-id-test"
|
||||
|
||||
config, err := NewOAuthConfig(testActiveDirectoryEndpoint, testTenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err)
|
||||
}
|
||||
|
||||
expected := "https://login.test.com/tenant-id-test/oauth2/authorize?api-version=1.0"
|
||||
if config.AuthorizeEndpoint.String() != expected {
|
||||
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint)
|
||||
}
|
||||
|
||||
expected = "https://login.test.com/tenant-id-test/oauth2/token?api-version=1.0"
|
||||
if config.TokenEndpoint.String() != expected {
|
||||
t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint)
|
||||
}
|
||||
|
||||
expected = "https://login.test.com/tenant-id-test/oauth2/devicecode?api-version=1.0"
|
||||
if config.DeviceCodeEndpoint.String() != expected {
|
||||
t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint)
|
||||
}
|
||||
}
|
||||
242
vendor/github.com/Azure/go-autorest/autorest/adal/devicetoken.go
generated
vendored
Normal file
242
vendor/github.com/Azure/go-autorest/autorest/adal/devicetoken.go
generated
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
This file is largely based on rjw57/oauth2device's code, with the follow differences:
|
||||
* scope -> resource, and only allow a single one
|
||||
* receive "Message" in the DeviceCode struct and show it to users as the prompt
|
||||
* azure-xplat-cli has the following behavior that this emulates:
|
||||
- does not send client_secret during the token exchange
|
||||
- sends resource again in the token exchange request
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
logPrefix = "autorest/adal/devicetoken:"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDeviceGeneric represents an unknown error from the token endpoint when using device flow
|
||||
ErrDeviceGeneric = fmt.Errorf("%s Error while retrieving OAuth token: Unknown Error", logPrefix)
|
||||
|
||||
// ErrDeviceAccessDenied represents an access denied error from the token endpoint when using device flow
|
||||
ErrDeviceAccessDenied = fmt.Errorf("%s Error while retrieving OAuth token: Access Denied", logPrefix)
|
||||
|
||||
// ErrDeviceAuthorizationPending represents the server waiting on the user to complete the device flow
|
||||
ErrDeviceAuthorizationPending = fmt.Errorf("%s Error while retrieving OAuth token: Authorization Pending", logPrefix)
|
||||
|
||||
// ErrDeviceCodeExpired represents the server timing out and expiring the code during device flow
|
||||
ErrDeviceCodeExpired = fmt.Errorf("%s Error while retrieving OAuth token: Code Expired", logPrefix)
|
||||
|
||||
// ErrDeviceSlowDown represents the service telling us we're polling too often during device flow
|
||||
ErrDeviceSlowDown = fmt.Errorf("%s Error while retrieving OAuth token: Slow Down", logPrefix)
|
||||
|
||||
// ErrDeviceCodeEmpty represents an empty device code from the device endpoint while using device flow
|
||||
ErrDeviceCodeEmpty = fmt.Errorf("%s Error while retrieving device code: Device Code Empty", logPrefix)
|
||||
|
||||
// ErrOAuthTokenEmpty represents an empty OAuth token from the token endpoint when using device flow
|
||||
ErrOAuthTokenEmpty = fmt.Errorf("%s Error while retrieving OAuth token: Token Empty", logPrefix)
|
||||
|
||||
errCodeSendingFails = "Error occurred while sending request for Device Authorization Code"
|
||||
errCodeHandlingFails = "Error occurred while handling response from the Device Endpoint"
|
||||
errTokenSendingFails = "Error occurred while sending request with device code for a token"
|
||||
errTokenHandlingFails = "Error occurred while handling response from the Token Endpoint (during device flow)"
|
||||
errStatusNotOK = "Error HTTP status != 200"
|
||||
)
|
||||
|
||||
// DeviceCode is the object returned by the device auth endpoint
|
||||
// It contains information to instruct the user to complete the auth flow
|
||||
type DeviceCode struct {
|
||||
DeviceCode *string `json:"device_code,omitempty"`
|
||||
UserCode *string `json:"user_code,omitempty"`
|
||||
VerificationURL *string `json:"verification_url,omitempty"`
|
||||
ExpiresIn *int64 `json:"expires_in,string,omitempty"`
|
||||
Interval *int64 `json:"interval,string,omitempty"`
|
||||
|
||||
Message *string `json:"message"` // Azure specific
|
||||
Resource string // store the following, stored when initiating, used when exchanging
|
||||
OAuthConfig OAuthConfig
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// TokenError is the object returned by the token exchange endpoint
|
||||
// when something is amiss
|
||||
type TokenError struct {
|
||||
Error *string `json:"error,omitempty"`
|
||||
ErrorCodes []int `json:"error_codes,omitempty"`
|
||||
ErrorDescription *string `json:"error_description,omitempty"`
|
||||
Timestamp *string `json:"timestamp,omitempty"`
|
||||
TraceID *string `json:"trace_id,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceToken is the object return by the token exchange endpoint
|
||||
// It can either look like a Token or an ErrorToken, so put both here
|
||||
// and check for presence of "Error" to know if we are in error state
|
||||
type deviceToken struct {
|
||||
Token
|
||||
TokenError
|
||||
}
|
||||
|
||||
// InitiateDeviceAuth initiates a device auth flow. It returns a DeviceCode
|
||||
// that can be used with CheckForUserCompletion or WaitForUserCompletion.
|
||||
func InitiateDeviceAuth(sender Sender, oauthConfig OAuthConfig, clientID, resource string) (*DeviceCode, error) {
|
||||
v := url.Values{
|
||||
"client_id": []string{clientID},
|
||||
"resource": []string{resource},
|
||||
}
|
||||
|
||||
s := v.Encode()
|
||||
body := ioutil.NopCloser(strings.NewReader(s))
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, oauthConfig.DeviceCodeEndpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
||||
}
|
||||
|
||||
req.ContentLength = int64(len(s))
|
||||
req.Header.Set(contentType, mimeTypeFormPost)
|
||||
resp, err := sender.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
rb, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, errStatusNotOK)
|
||||
}
|
||||
|
||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||
return nil, ErrDeviceCodeEmpty
|
||||
}
|
||||
|
||||
var code DeviceCode
|
||||
err = json.Unmarshal(rb, &code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
code.ClientID = clientID
|
||||
code.Resource = resource
|
||||
code.OAuthConfig = oauthConfig
|
||||
|
||||
return &code, nil
|
||||
}
|
||||
|
||||
// CheckForUserCompletion takes a DeviceCode and checks with the Azure AD OAuth endpoint
|
||||
// to see if the device flow has: been completed, timed out, or otherwise failed
|
||||
func CheckForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
||||
v := url.Values{
|
||||
"client_id": []string{code.ClientID},
|
||||
"code": []string{*code.DeviceCode},
|
||||
"grant_type": []string{OAuthGrantTypeDeviceCode},
|
||||
"resource": []string{code.Resource},
|
||||
}
|
||||
|
||||
s := v.Encode()
|
||||
body := ioutil.NopCloser(strings.NewReader(s))
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, code.OAuthConfig.TokenEndpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
||||
}
|
||||
|
||||
req.ContentLength = int64(len(s))
|
||||
req.Header.Set(contentType, mimeTypeFormPost)
|
||||
resp, err := sender.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
rb, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && len(strings.Trim(string(rb), " ")) == 0 {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, errStatusNotOK)
|
||||
}
|
||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||
return nil, ErrOAuthTokenEmpty
|
||||
}
|
||||
|
||||
var token deviceToken
|
||||
err = json.Unmarshal(rb, &token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if token.Error == nil {
|
||||
return &token.Token, nil
|
||||
}
|
||||
|
||||
switch *token.Error {
|
||||
case "authorization_pending":
|
||||
return nil, ErrDeviceAuthorizationPending
|
||||
case "slow_down":
|
||||
return nil, ErrDeviceSlowDown
|
||||
case "access_denied":
|
||||
return nil, ErrDeviceAccessDenied
|
||||
case "code_expired":
|
||||
return nil, ErrDeviceCodeExpired
|
||||
default:
|
||||
return nil, ErrDeviceGeneric
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForUserCompletion calls CheckForUserCompletion repeatedly until a token is granted or an error state occurs.
|
||||
// This prevents the user from looping and checking against 'ErrDeviceAuthorizationPending'.
|
||||
func WaitForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
||||
intervalDuration := time.Duration(*code.Interval) * time.Second
|
||||
waitDuration := intervalDuration
|
||||
|
||||
for {
|
||||
token, err := CheckForUserCompletion(sender, code)
|
||||
|
||||
if err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
switch err {
|
||||
case ErrDeviceSlowDown:
|
||||
waitDuration += waitDuration
|
||||
case ErrDeviceAuthorizationPending:
|
||||
// noop
|
||||
default: // everything else is "fatal" to us
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if waitDuration > (intervalDuration * 3) {
|
||||
return nil, fmt.Errorf("%s Error waiting for user to complete device flow. Server told us to slow_down too much", logPrefix)
|
||||
}
|
||||
|
||||
time.Sleep(waitDuration)
|
||||
}
|
||||
}
|
||||
330
vendor/github.com/Azure/go-autorest/autorest/adal/devicetoken_test.go
generated
vendored
Normal file
330
vendor/github.com/Azure/go-autorest/autorest/adal/devicetoken_test.go
generated
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
TestResource = "SomeResource"
|
||||
TestClientID = "SomeClientID"
|
||||
TestTenantID = "SomeTenantID"
|
||||
TestActiveDirectoryEndpoint = "https://login.test.com/"
|
||||
)
|
||||
|
||||
var (
|
||||
testOAuthConfig, _ = NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
|
||||
TestOAuthConfig = *testOAuthConfig
|
||||
)
|
||||
|
||||
const MockDeviceCodeResponse = `
|
||||
{
|
||||
"device_code": "10000-40-1234567890",
|
||||
"user_code": "ABCDEF",
|
||||
"verification_url": "http://aka.ms/deviceauth",
|
||||
"expires_in": "900",
|
||||
"interval": "0"
|
||||
}
|
||||
`
|
||||
|
||||
const MockDeviceTokenResponse = `{
|
||||
"access_token": "accessToken",
|
||||
"refresh_token": "refreshToken",
|
||||
"expires_in": "1000",
|
||||
"expires_on": "2000",
|
||||
"not_before": "3000",
|
||||
"resource": "resource",
|
||||
"token_type": "type"
|
||||
}
|
||||
`
|
||||
|
||||
func TestDeviceCodeIncludesResource(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
sender.AppendResponse(mocks.NewResponseWithContent(MockDeviceCodeResponse))
|
||||
|
||||
code, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
|
||||
if err != nil {
|
||||
t.Fatalf("adal: unexpected error initiating device auth")
|
||||
}
|
||||
|
||||
if code.Resource != TestResource {
|
||||
t.Fatalf("adal: InitiateDeviceAuth failed to stash the resource in the DeviceCode struct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceCodeReturnsErrorIfSendingFails(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
sender.SetError(fmt.Errorf("this is an error"))
|
||||
|
||||
_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
|
||||
if err == nil || !strings.Contains(err.Error(), errCodeSendingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeSendingFails, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceCodeReturnsErrorIfBadRequest(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody("doesn't matter")
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
|
||||
if err == nil || !strings.Contains(err.Error(), errCodeHandlingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceCodeReturnsErrorIfCannotDeserializeDeviceCode(t *testing.T) {
|
||||
gibberishJSON := strings.Replace(MockDeviceCodeResponse, "expires_in", "\":, :gibberish", -1)
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(gibberishJSON)
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
|
||||
|
||||
_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
|
||||
if err == nil || !strings.Contains(err.Error(), errCodeHandlingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errCodeHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceCodeReturnsErrorIfEmptyDeviceCode(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody("")
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
|
||||
|
||||
_, err := InitiateDeviceAuth(sender, TestOAuthConfig, TestClientID, TestResource)
|
||||
if err != ErrDeviceCodeEmpty {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", ErrDeviceCodeEmpty, err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func deviceCode() *DeviceCode {
|
||||
var deviceCode DeviceCode
|
||||
_ = json.Unmarshal([]byte(MockDeviceCodeResponse), &deviceCode)
|
||||
deviceCode.Resource = TestResource
|
||||
deviceCode.ClientID = TestClientID
|
||||
return &deviceCode
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturns(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(MockDeviceTokenResponse)
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != nil {
|
||||
t.Fatalf("adal: got error unexpectedly")
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfSendingFails(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
sender.SetError(fmt.Errorf("this is an error"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err == nil || !strings.Contains(err.Error(), errTokenSendingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenSendingFails, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfServerError(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody("")
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusInternalServerError, "Internal Server Error"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err == nil || !strings.Contains(err.Error(), errTokenHandlingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfCannotDeserializeDeviceToken(t *testing.T) {
|
||||
gibberishJSON := strings.Replace(MockDeviceTokenResponse, "expires_in", ";:\"gibberish", -1)
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(gibberishJSON)
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err == nil || !strings.Contains(err.Error(), errTokenHandlingFails) {
|
||||
t.Fatalf("adal: failed to get correct error expected(%s) actual(%s)", errTokenHandlingFails, err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func errorDeviceTokenResponse(message string) string {
|
||||
return `{ "error": "` + message + `" }`
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfAuthorizationPending(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(errorDeviceTokenResponse("authorization_pending"))
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := CheckForUserCompletion(sender, deviceCode())
|
||||
if err != ErrDeviceAuthorizationPending {
|
||||
t.Fatalf("!!!")
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfSlowDown(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(errorDeviceTokenResponse("slow_down"))
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := CheckForUserCompletion(sender, deviceCode())
|
||||
if err != ErrDeviceSlowDown {
|
||||
t.Fatalf("!!!")
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
type deviceTokenSender struct {
|
||||
errorString string
|
||||
attempts int
|
||||
}
|
||||
|
||||
func newDeviceTokenSender(deviceErrorString string) *deviceTokenSender {
|
||||
return &deviceTokenSender{errorString: deviceErrorString, attempts: 0}
|
||||
}
|
||||
|
||||
func (s *deviceTokenSender) Do(req *http.Request) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
if s.attempts < 1 {
|
||||
s.attempts++
|
||||
resp = mocks.NewResponseWithContent(errorDeviceTokenResponse(s.errorString))
|
||||
} else {
|
||||
resp = mocks.NewResponseWithContent(MockDeviceTokenResponse)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// since the above only exercise CheckForUserCompletion, we repeat the test here,
|
||||
// but with the intent of showing that WaitForUserCompletion loops properly.
|
||||
func TestDeviceTokenSucceedsWithIntermediateAuthPending(t *testing.T) {
|
||||
sender := newDeviceTokenSender("authorization_pending")
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// same as above but with SlowDown now
|
||||
func TestDeviceTokenSucceedsWithIntermediateSlowDown(t *testing.T) {
|
||||
sender := newDeviceTokenSender("slow_down")
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfAccessDenied(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(errorDeviceTokenResponse("access_denied"))
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != ErrDeviceAccessDenied {
|
||||
t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceAccessDenied.Error(), err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfCodeExpired(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(errorDeviceTokenResponse("code_expired"))
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != ErrDeviceCodeExpired {
|
||||
t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceCodeExpired.Error(), err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorForUnknownError(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody(errorDeviceTokenResponse("unknown_error"))
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "Bad Request"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err == nil {
|
||||
t.Fatalf("failed to get error")
|
||||
}
|
||||
if err != ErrDeviceGeneric {
|
||||
t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrDeviceGeneric.Error(), err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceTokenReturnsErrorIfTokenEmptyAndStatusOK(t *testing.T) {
|
||||
sender := mocks.NewSender()
|
||||
body := mocks.NewBody("")
|
||||
sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK"))
|
||||
|
||||
_, err := WaitForUserCompletion(sender, deviceCode())
|
||||
if err != ErrOAuthTokenEmpty {
|
||||
t.Fatalf("adal: got wrong error expected(%s) actual(%s)", ErrOAuthTokenEmpty.Error(), err.Error())
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("response body was left open!")
|
||||
}
|
||||
}
|
||||
20
vendor/github.com/Azure/go-autorest/autorest/adal/msi.go
generated
vendored
Normal file
20
vendor/github.com/Azure/go-autorest/autorest/adal/msi.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// +build !windows
|
||||
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// msiPath is the path to the MSI Extension settings file (to discover the endpoint)
|
||||
var msiPath = "/var/lib/waagent/ManagedIdentity-Settings"
|
||||
25
vendor/github.com/Azure/go-autorest/autorest/adal/msi_windows.go
generated
vendored
Normal file
25
vendor/github.com/Azure/go-autorest/autorest/adal/msi_windows.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
// +build windows
|
||||
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// msiPath is the path to the MSI Extension settings file (to discover the endpoint)
|
||||
var msiPath = strings.Join([]string{os.Getenv("SystemDrive"), "WindowsAzure/Config/ManagedIdentity-Settings"}, "/")
|
||||
73
vendor/github.com/Azure/go-autorest/autorest/adal/persist.go
generated
vendored
Normal file
73
vendor/github.com/Azure/go-autorest/autorest/adal/persist.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// LoadToken restores a Token object from a file located at 'path'.
|
||||
func LoadToken(path string) (*Token, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var token Token
|
||||
|
||||
dec := json.NewDecoder(file)
|
||||
if err = dec.Decode(&token); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode contents of file (%s) into Token representation: %v", path, err)
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// SaveToken persists an oauth token at the given location on disk.
|
||||
// It moves the new file into place so it can safely be used to replace an existing file
|
||||
// that maybe accessed by multiple processes.
|
||||
func SaveToken(path string, mode os.FileMode, token Token) error {
|
||||
dir := filepath.Dir(path)
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory (%s) to store token in: %v", dir, err)
|
||||
}
|
||||
|
||||
newFile, err := ioutil.TempFile(dir, "token")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the temp file to write the token: %v", err)
|
||||
}
|
||||
tempPath := newFile.Name()
|
||||
|
||||
if err := json.NewEncoder(newFile).Encode(token); err != nil {
|
||||
return fmt.Errorf("failed to encode token to file (%s) while saving token: %v", tempPath, err)
|
||||
}
|
||||
if err := newFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file %s: %v", tempPath, err)
|
||||
}
|
||||
|
||||
// Atomic replace to avoid multi-writer file corruptions
|
||||
if err := os.Rename(tempPath, path); err != nil {
|
||||
return fmt.Errorf("failed to move temporary token to desired output location. src=%s dst=%s: %v", tempPath, path, err)
|
||||
}
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
return fmt.Errorf("failed to chmod the token file %s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
171
vendor/github.com/Azure/go-autorest/autorest/adal/persist_test.go
generated
vendored
Normal file
171
vendor/github.com/Azure/go-autorest/autorest/adal/persist_test.go
generated
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const MockTokenJSON string = `{
|
||||
"access_token": "accessToken",
|
||||
"refresh_token": "refreshToken",
|
||||
"expires_in": "1000",
|
||||
"expires_on": "2000",
|
||||
"not_before": "3000",
|
||||
"resource": "resource",
|
||||
"token_type": "type"
|
||||
}`
|
||||
|
||||
var TestToken = Token{
|
||||
AccessToken: "accessToken",
|
||||
RefreshToken: "refreshToken",
|
||||
ExpiresIn: "1000",
|
||||
ExpiresOn: "2000",
|
||||
NotBefore: "3000",
|
||||
Resource: "resource",
|
||||
Type: "type",
|
||||
}
|
||||
|
||||
func writeTestTokenFile(t *testing.T, suffix string, contents string) *os.File {
|
||||
f, err := ioutil.TempFile(os.TempDir(), suffix)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: unexpected error when creating temp file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write([]byte(contents))
|
||||
if err != nil {
|
||||
t.Fatalf("azure: unexpected error when writing temp test file: %v", err)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func TestLoadToken(t *testing.T) {
|
||||
f := writeTestTokenFile(t, "testloadtoken", MockTokenJSON)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
expectedToken := TestToken
|
||||
actualToken, err := LoadToken(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("azure: unexpected error loading token from file: %v", err)
|
||||
}
|
||||
|
||||
if *actualToken != expectedToken {
|
||||
t.Fatalf("azure: failed to decode properly expected(%v) actual(%v)", expectedToken, *actualToken)
|
||||
}
|
||||
|
||||
// test that LoadToken closes the file properly
|
||||
err = SaveToken(f.Name(), 0600, *actualToken)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: could not save token after LoadToken: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTokenFailsBadPath(t *testing.T) {
|
||||
_, err := LoadToken("/tmp/this_file_should_never_exist_really")
|
||||
expectedSubstring := "failed to open file"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedSubstring) {
|
||||
t.Fatalf("azure: failed to get correct error expected(%s) actual(%s)", expectedSubstring, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTokenFailsBadJson(t *testing.T) {
|
||||
gibberishJSON := strings.Replace(MockTokenJSON, "expires_on", ";:\"gibberish", -1)
|
||||
f := writeTestTokenFile(t, "testloadtokenfailsbadjson", gibberishJSON)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err := LoadToken(f.Name())
|
||||
expectedSubstring := "failed to decode contents of file"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedSubstring) {
|
||||
t.Fatalf("azure: failed to get correct error expected(%s) actual(%s)", expectedSubstring, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func token() *Token {
|
||||
var token Token
|
||||
json.Unmarshal([]byte(MockTokenJSON), &token)
|
||||
return &token
|
||||
}
|
||||
|
||||
func TestSaveToken(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "testloadtoken")
|
||||
if err != nil {
|
||||
t.Fatalf("azure: unexpected error when creating temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
f.Close()
|
||||
|
||||
mode := os.ModePerm & 0642
|
||||
err = SaveToken(f.Name(), mode, *token())
|
||||
if err != nil {
|
||||
t.Fatalf("azure: unexpected error saving token to file: %v", err)
|
||||
}
|
||||
fi, err := os.Stat(f.Name()) // open a new stat as held ones are not fresh
|
||||
if err != nil {
|
||||
t.Fatalf("azure: stat failed: %v", err)
|
||||
}
|
||||
if runtime.GOOS != "windows" { // permissions don't work on Windows
|
||||
if perm := fi.Mode().Perm(); perm != mode {
|
||||
t.Fatalf("azure: wrong file perm. got:%s; expected:%s file :%s", perm, mode, f.Name())
|
||||
}
|
||||
}
|
||||
|
||||
var actualToken Token
|
||||
var expectedToken Token
|
||||
|
||||
json.Unmarshal([]byte(MockTokenJSON), expectedToken)
|
||||
|
||||
contents, err := ioutil.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
t.Fatal("!!")
|
||||
}
|
||||
json.Unmarshal(contents, actualToken)
|
||||
|
||||
if !reflect.DeepEqual(actualToken, expectedToken) {
|
||||
t.Fatal("azure: token was not serialized correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveTokenFailsNoPermission(t *testing.T) {
|
||||
pathWhereWeShouldntHavePermission := "/usr/thiswontwork/atall"
|
||||
if runtime.GOOS == "windows" {
|
||||
pathWhereWeShouldntHavePermission = path.Join(os.Getenv("windir"), "system32\\mytokendir\\mytoken")
|
||||
}
|
||||
err := SaveToken(pathWhereWeShouldntHavePermission, 0644, *token())
|
||||
expectedSubstring := "failed to create directory"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedSubstring) {
|
||||
t.Fatalf("azure: failed to get correct error expected(%s) actual(%v)", expectedSubstring, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveTokenFailsCantCreate(t *testing.T) {
|
||||
tokenPath := "/thiswontwork"
|
||||
if runtime.GOOS == "windows" {
|
||||
tokenPath = path.Join(os.Getenv("windir"), "system32")
|
||||
}
|
||||
err := SaveToken(tokenPath, 0644, *token())
|
||||
expectedSubstring := "failed to create the temp file to write the token"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedSubstring) {
|
||||
t.Fatalf("azure: failed to get correct error expected(%s) actual(%v)", expectedSubstring, err)
|
||||
}
|
||||
}
|
||||
60
vendor/github.com/Azure/go-autorest/autorest/adal/sender.go
generated
vendored
Normal file
60
vendor/github.com/Azure/go-autorest/autorest/adal/sender.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "Content-Type"
|
||||
mimeTypeFormPost = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
// Sender is the interface that wraps the Do method to send HTTP requests.
|
||||
//
|
||||
// The standard http.Client conforms to this interface.
|
||||
type Sender interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// SenderFunc is a method that implements the Sender interface.
|
||||
type SenderFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
// Do implements the Sender interface on SenderFunc.
|
||||
func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
|
||||
return sf(r)
|
||||
}
|
||||
|
||||
// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the
|
||||
// http.Request and pass it along or, first, pass the http.Request along then react to the
|
||||
// http.Response result.
|
||||
type SendDecorator func(Sender) Sender
|
||||
|
||||
// CreateSender creates, decorates, and returns, as a Sender, the default http.Client.
|
||||
func CreateSender(decorators ...SendDecorator) Sender {
|
||||
return DecorateSender(&http.Client{}, decorators...)
|
||||
}
|
||||
|
||||
// DecorateSender accepts a Sender and a, possibly empty, set of SendDecorators, which is applies to
|
||||
// the Sender. Decorators are applied in the order received, but their affect upon the request
|
||||
// depends on whether they are a pre-decorator (change the http.Request and then pass it along) or a
|
||||
// post-decorator (pass the http.Request along and react to the results in http.Response).
|
||||
func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
|
||||
for _, decorate := range decorators {
|
||||
s = decorate(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
427
vendor/github.com/Azure/go-autorest/autorest/adal/token.go
generated
vendored
Normal file
427
vendor/github.com/Azure/go-autorest/autorest/adal/token.go
generated
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/date"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRefresh = 5 * time.Minute
|
||||
|
||||
// OAuthGrantTypeDeviceCode is the "grant_type" identifier used in device flow
|
||||
OAuthGrantTypeDeviceCode = "device_code"
|
||||
|
||||
// OAuthGrantTypeClientCredentials is the "grant_type" identifier used in credential flows
|
||||
OAuthGrantTypeClientCredentials = "client_credentials"
|
||||
|
||||
// OAuthGrantTypeRefreshToken is the "grant_type" identifier used in refresh token flows
|
||||
OAuthGrantTypeRefreshToken = "refresh_token"
|
||||
|
||||
// metadataHeader is the header required by MSI extension
|
||||
metadataHeader = "Metadata"
|
||||
)
|
||||
|
||||
// OAuthTokenProvider is an interface which should be implemented by an access token retriever
|
||||
type OAuthTokenProvider interface {
|
||||
OAuthToken() string
|
||||
}
|
||||
|
||||
// Refresher is an interface for token refresh functionality
|
||||
type Refresher interface {
|
||||
Refresh() error
|
||||
RefreshExchange(resource string) error
|
||||
EnsureFresh() error
|
||||
}
|
||||
|
||||
// TokenRefreshCallback is the type representing callbacks that will be called after
|
||||
// a successful token refresh
|
||||
type TokenRefreshCallback func(Token) error
|
||||
|
||||
// Token encapsulates the access token used to authorize Azure requests.
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
ExpiresOn string `json:"expires_on"`
|
||||
NotBefore string `json:"not_before"`
|
||||
|
||||
Resource string `json:"resource"`
|
||||
Type string `json:"token_type"`
|
||||
}
|
||||
|
||||
// Expires returns the time.Time when the Token expires.
|
||||
func (t Token) Expires() time.Time {
|
||||
s, err := strconv.Atoi(t.ExpiresOn)
|
||||
if err != nil {
|
||||
s = -3600
|
||||
}
|
||||
|
||||
expiration := date.NewUnixTimeFromSeconds(float64(s))
|
||||
|
||||
return time.Time(expiration).UTC()
|
||||
}
|
||||
|
||||
// IsExpired returns true if the Token is expired, false otherwise.
|
||||
func (t Token) IsExpired() bool {
|
||||
return t.WillExpireIn(0)
|
||||
}
|
||||
|
||||
// WillExpireIn returns true if the Token will expire after the passed time.Duration interval
|
||||
// from now, false otherwise.
|
||||
func (t Token) WillExpireIn(d time.Duration) bool {
|
||||
return !t.Expires().After(time.Now().Add(d))
|
||||
}
|
||||
|
||||
//OAuthToken return the current access token
|
||||
func (t *Token) OAuthToken() string {
|
||||
return t.AccessToken
|
||||
}
|
||||
|
||||
// ServicePrincipalNoSecret represents a secret type that contains no secret
|
||||
// meaning it is not valid for fetching a fresh token. This is used by Manual
|
||||
type ServicePrincipalNoSecret struct {
|
||||
}
|
||||
|
||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret
|
||||
// It only returns an error for the ServicePrincipalNoSecret type
|
||||
func (noSecret *ServicePrincipalNoSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||
return fmt.Errorf("Manually created ServicePrincipalToken does not contain secret material to retrieve a new access token")
|
||||
}
|
||||
|
||||
// ServicePrincipalSecret is an interface that allows various secret mechanism to fill the form
|
||||
// that is submitted when acquiring an oAuth token.
|
||||
type ServicePrincipalSecret interface {
|
||||
SetAuthenticationValues(spt *ServicePrincipalToken, values *url.Values) error
|
||||
}
|
||||
|
||||
// ServicePrincipalTokenSecret implements ServicePrincipalSecret for client_secret type authorization.
|
||||
type ServicePrincipalTokenSecret struct {
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||
// It will populate the form submitted during oAuth Token Acquisition using the client_secret.
|
||||
func (tokenSecret *ServicePrincipalTokenSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||
v.Set("client_secret", tokenSecret.ClientSecret)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServicePrincipalCertificateSecret implements ServicePrincipalSecret for generic RSA cert auth with signed JWTs.
|
||||
type ServicePrincipalCertificateSecret struct {
|
||||
Certificate *x509.Certificate
|
||||
PrivateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
|
||||
type ServicePrincipalMSISecret struct {
|
||||
}
|
||||
|
||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||
func (msiSecret *ServicePrincipalMSISecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignJwt returns the JWT signed with the certificate's private key.
|
||||
func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalToken) (string, error) {
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(secret.Certificate.Raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
thumbprint := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
// The jti (JWT ID) claim provides a unique identifier for the JWT.
|
||||
jti := make([]byte, 20)
|
||||
_, err = rand.Read(jti)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := jwt.New(jwt.SigningMethodRS256)
|
||||
token.Header["x5t"] = thumbprint
|
||||
token.Claims = jwt.MapClaims{
|
||||
"aud": spt.oauthConfig.TokenEndpoint.String(),
|
||||
"iss": spt.clientID,
|
||||
"sub": spt.clientID,
|
||||
"jti": base64.URLEncoding.EncodeToString(jti),
|
||||
"nbf": time.Now().Unix(),
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
}
|
||||
|
||||
signedString, err := token.SignedString(secret.PrivateKey)
|
||||
return signedString, err
|
||||
}
|
||||
|
||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||
// It will populate the form submitted during oAuth Token Acquisition using a JWT signed with a certificate.
|
||||
func (secret *ServicePrincipalCertificateSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||
jwt, err := secret.SignJwt(spt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.Set("client_assertion", jwt)
|
||||
v.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServicePrincipalToken encapsulates a Token created for a Service Principal.
|
||||
type ServicePrincipalToken struct {
|
||||
Token
|
||||
|
||||
secret ServicePrincipalSecret
|
||||
oauthConfig OAuthConfig
|
||||
clientID string
|
||||
resource string
|
||||
autoRefresh bool
|
||||
refreshWithin time.Duration
|
||||
sender Sender
|
||||
|
||||
refreshCallbacks []TokenRefreshCallback
|
||||
}
|
||||
|
||||
// NewServicePrincipalTokenWithSecret create a ServicePrincipalToken using the supplied ServicePrincipalSecret implementation.
|
||||
func NewServicePrincipalTokenWithSecret(oauthConfig OAuthConfig, id string, resource string, secret ServicePrincipalSecret, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||
spt := &ServicePrincipalToken{
|
||||
oauthConfig: oauthConfig,
|
||||
secret: secret,
|
||||
clientID: id,
|
||||
resource: resource,
|
||||
autoRefresh: true,
|
||||
refreshWithin: defaultRefresh,
|
||||
sender: &http.Client{},
|
||||
refreshCallbacks: callbacks,
|
||||
}
|
||||
return spt, nil
|
||||
}
|
||||
|
||||
// NewServicePrincipalTokenFromManualToken creates a ServicePrincipalToken using the supplied token
|
||||
func NewServicePrincipalTokenFromManualToken(oauthConfig OAuthConfig, clientID string, resource string, token Token, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||
spt, err := NewServicePrincipalTokenWithSecret(
|
||||
oauthConfig,
|
||||
clientID,
|
||||
resource,
|
||||
&ServicePrincipalNoSecret{},
|
||||
callbacks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spt.Token = token
|
||||
|
||||
return spt, nil
|
||||
}
|
||||
|
||||
// NewServicePrincipalToken creates a ServicePrincipalToken from the supplied Service Principal
|
||||
// credentials scoped to the named resource.
|
||||
func NewServicePrincipalToken(oauthConfig OAuthConfig, clientID string, secret string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||
return NewServicePrincipalTokenWithSecret(
|
||||
oauthConfig,
|
||||
clientID,
|
||||
resource,
|
||||
&ServicePrincipalTokenSecret{
|
||||
ClientSecret: secret,
|
||||
},
|
||||
callbacks...,
|
||||
)
|
||||
}
|
||||
|
||||
// NewServicePrincipalTokenFromCertificate create a ServicePrincipalToken from the supplied pkcs12 bytes.
|
||||
func NewServicePrincipalTokenFromCertificate(oauthConfig OAuthConfig, clientID string, certificate *x509.Certificate, privateKey *rsa.PrivateKey, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||
return NewServicePrincipalTokenWithSecret(
|
||||
oauthConfig,
|
||||
clientID,
|
||||
resource,
|
||||
&ServicePrincipalCertificateSecret{
|
||||
PrivateKey: privateKey,
|
||||
Certificate: certificate,
|
||||
},
|
||||
callbacks...,
|
||||
)
|
||||
}
|
||||
|
||||
// GetMSIVMEndpoint gets the MSI endpoint on Virtual Machines.
|
||||
func GetMSIVMEndpoint() (string, error) {
|
||||
return getMSIVMEndpoint(msiPath)
|
||||
}
|
||||
|
||||
func getMSIVMEndpoint(path string) (string, error) {
|
||||
// Read MSI settings
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
msiSettings := struct {
|
||||
URL string `json:"url"`
|
||||
}{}
|
||||
err = json.Unmarshal(bytes, &msiSettings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return msiSettings.URL, nil
|
||||
}
|
||||
|
||||
// NewServicePrincipalTokenFromMSI creates a ServicePrincipalToken via the MSI VM Extension.
|
||||
func NewServicePrincipalTokenFromMSI(msiEndpoint, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||
// We set the oauth config token endpoint to be MSI's endpoint
|
||||
msiEndpointURL, err := url.Parse(msiEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oauthConfig, err := NewOAuthConfig(msiEndpointURL.String(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spt := &ServicePrincipalToken{
|
||||
oauthConfig: *oauthConfig,
|
||||
secret: &ServicePrincipalMSISecret{},
|
||||
resource: resource,
|
||||
autoRefresh: true,
|
||||
refreshWithin: defaultRefresh,
|
||||
sender: &http.Client{},
|
||||
refreshCallbacks: callbacks,
|
||||
}
|
||||
|
||||
return spt, nil
|
||||
}
|
||||
|
||||
// EnsureFresh will refresh the token if it will expire within the refresh window (as set by
|
||||
// RefreshWithin) and autoRefresh flag is on.
|
||||
func (spt *ServicePrincipalToken) EnsureFresh() error {
|
||||
if spt.autoRefresh && spt.WillExpireIn(spt.refreshWithin) {
|
||||
return spt.Refresh()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvokeRefreshCallbacks calls any TokenRefreshCallbacks that were added to the SPT during initialization
|
||||
func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
|
||||
if spt.refreshCallbacks != nil {
|
||||
for _, callback := range spt.refreshCallbacks {
|
||||
err := callback(spt.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: TokenRefreshCallback handler failed. Error = '%v'", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh obtains a fresh token for the Service Principal.
|
||||
func (spt *ServicePrincipalToken) Refresh() error {
|
||||
return spt.refreshInternal(spt.resource)
|
||||
}
|
||||
|
||||
// RefreshExchange refreshes the token, but for a different resource.
|
||||
func (spt *ServicePrincipalToken) RefreshExchange(resource string) error {
|
||||
return spt.refreshInternal(resource)
|
||||
}
|
||||
|
||||
func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
|
||||
v := url.Values{}
|
||||
v.Set("client_id", spt.clientID)
|
||||
v.Set("resource", resource)
|
||||
|
||||
if spt.RefreshToken != "" {
|
||||
v.Set("grant_type", OAuthGrantTypeRefreshToken)
|
||||
v.Set("refresh_token", spt.RefreshToken)
|
||||
} else {
|
||||
v.Set("grant_type", OAuthGrantTypeClientCredentials)
|
||||
err := spt.secret.SetAuthenticationValues(spt, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s := v.Encode()
|
||||
body := ioutil.NopCloser(strings.NewReader(s))
|
||||
req, err := http.NewRequest(http.MethodPost, spt.oauthConfig.TokenEndpoint.String(), body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
|
||||
}
|
||||
|
||||
req.ContentLength = int64(len(s))
|
||||
req.Header.Set(contentType, mimeTypeFormPost)
|
||||
if _, ok := spt.secret.(*ServicePrincipalMSISecret); ok {
|
||||
req.Header.Set(metadataHeader, "true")
|
||||
}
|
||||
resp, err := spt.sender.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: Failed to execute the refresh request. Error = '%v'", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
rb, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body", resp.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("adal: Refresh request failed. Status Code = '%d'. Response body: %s", resp.StatusCode, string(rb))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: Failed to read a new service principal token during refresh. Error = '%v'", err)
|
||||
}
|
||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||
return fmt.Errorf("adal: Empty service principal token received during refresh")
|
||||
}
|
||||
var token Token
|
||||
err = json.Unmarshal(rb, &token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb))
|
||||
}
|
||||
|
||||
spt.Token = token
|
||||
|
||||
return spt.InvokeRefreshCallbacks(token)
|
||||
}
|
||||
|
||||
// SetAutoRefresh enables or disables automatic refreshing of stale tokens.
|
||||
func (spt *ServicePrincipalToken) SetAutoRefresh(autoRefresh bool) {
|
||||
spt.autoRefresh = autoRefresh
|
||||
}
|
||||
|
||||
// SetRefreshWithin sets the interval within which if the token will expire, EnsureFresh will
|
||||
// refresh the token.
|
||||
func (spt *ServicePrincipalToken) SetRefreshWithin(d time.Duration) {
|
||||
spt.refreshWithin = d
|
||||
return
|
||||
}
|
||||
|
||||
// SetSender sets the http.Client used when obtaining the Service Principal token. An
|
||||
// undecorated http.Client is used by default.
|
||||
func (spt *ServicePrincipalToken) SetSender(s Sender) { spt.sender = s }
|
||||
654
vendor/github.com/Azure/go-autorest/autorest/adal/token_test.go
generated
vendored
Normal file
654
vendor/github.com/Azure/go-autorest/autorest/adal/token_test.go
generated
vendored
Normal file
@@ -0,0 +1,654 @@
|
||||
package adal
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/date"
|
||||
"github.com/Azure/go-autorest/autorest/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFormData = "client_id=id&client_secret=secret&grant_type=client_credentials&resource=resource"
|
||||
defaultManualFormData = "client_id=id&grant_type=refresh_token&refresh_token=refreshtoken&resource=resource"
|
||||
)
|
||||
|
||||
func TestTokenExpires(t *testing.T) {
|
||||
tt := time.Now().Add(5 * time.Second)
|
||||
tk := newTokenExpiresAt(tt)
|
||||
|
||||
if tk.Expires().Equal(tt) {
|
||||
t.Fatalf("adal: Token#Expires miscalculated expiration time -- received %v, expected %v", tk.Expires(), tt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenIsExpired(t *testing.T) {
|
||||
tk := newTokenExpiresAt(time.Now().Add(-5 * time.Second))
|
||||
|
||||
if !tk.IsExpired() {
|
||||
t.Fatalf("adal: Token#IsExpired failed to mark a stale token as expired -- now %v, token expires at %v",
|
||||
time.Now().UTC(), tk.Expires())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenIsExpiredUninitialized(t *testing.T) {
|
||||
tk := &Token{}
|
||||
|
||||
if !tk.IsExpired() {
|
||||
t.Fatalf("adal: An uninitialized Token failed to mark itself as expired (expiration time %v)", tk.Expires())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenIsNoExpired(t *testing.T) {
|
||||
tk := newTokenExpiresAt(time.Now().Add(1000 * time.Second))
|
||||
|
||||
if tk.IsExpired() {
|
||||
t.Fatalf("adal: Token marked a fresh token as expired -- now %v, token expires at %v", time.Now().UTC(), tk.Expires())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenWillExpireIn(t *testing.T) {
|
||||
d := 5 * time.Second
|
||||
tk := newTokenExpiresIn(d)
|
||||
|
||||
if !tk.WillExpireIn(d) {
|
||||
t.Fatal("adal: Token#WillExpireIn mismeasured expiration time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenSetAutoRefresh(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
if !spt.autoRefresh {
|
||||
t.Fatal("adal: ServicePrincipalToken did not default to automatic token refreshing")
|
||||
}
|
||||
|
||||
spt.SetAutoRefresh(false)
|
||||
if spt.autoRefresh {
|
||||
t.Fatal("adal: ServicePrincipalToken#SetAutoRefresh did not disable automatic token refreshing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenSetRefreshWithin(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
if spt.refreshWithin != defaultRefresh {
|
||||
t.Fatal("adal: ServicePrincipalToken did not correctly set the default refresh interval")
|
||||
}
|
||||
|
||||
spt.SetRefreshWithin(2 * defaultRefresh)
|
||||
if spt.refreshWithin != 2*defaultRefresh {
|
||||
t.Fatal("adal: ServicePrincipalToken#SetRefreshWithin did not set the refresh interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenSetSender(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
c := &http.Client{}
|
||||
spt.SetSender(c)
|
||||
if !reflect.DeepEqual(c, spt.sender) {
|
||||
t.Fatal("adal: ServicePrincipalToken#SetSender did not set the sender")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshUsesPOST(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set HTTP method -- expected %v, received %v", "POST", r.Method)
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("the response was not closed!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenFromMSIRefreshUsesPOST(t *testing.T) {
|
||||
resource := "https://resource"
|
||||
cb := func(token Token) error { return nil }
|
||||
|
||||
spt, err := NewServicePrincipalTokenFromMSI("http://msiendpoint/", resource, cb)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get MSI SPT: %v", err)
|
||||
}
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set HTTP method -- expected %v, received %v", "POST", r.Method)
|
||||
}
|
||||
if h := r.Header.Get("Metadata"); h != "true" {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set Metadata header for MSI")
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err = spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
|
||||
if body.IsOpen() {
|
||||
t.Fatalf("the response was not closed!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshSetsMimeType(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.Header.Get(http.CanonicalHeaderKey("Content-Type")) != "application/x-www-form-urlencoded" {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set Content-Type -- expected %v, received %v",
|
||||
"application/x-form-urlencoded",
|
||||
r.Header.Get(http.CanonicalHeaderKey("Content-Type")))
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshSetsURL(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.String() != TestOAuthConfig.TokenEndpoint.String() {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set the URL -- expected %v, received %v",
|
||||
TestOAuthConfig.TokenEndpoint, r.URL)
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testServicePrincipalTokenRefreshSetsBody(t *testing.T, spt *ServicePrincipalToken, f func(*testing.T, []byte)) {
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("adal: Failed to read body of Service Principal token request (%v)", err)
|
||||
}
|
||||
f(t, b)
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenManualRefreshSetsBody(t *testing.T) {
|
||||
sptManual := newServicePrincipalTokenManual()
|
||||
testServicePrincipalTokenRefreshSetsBody(t, sptManual, func(t *testing.T, b []byte) {
|
||||
if string(b) != defaultManualFormData {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set the HTTP Request Body -- expected %v, received %v",
|
||||
defaultManualFormData, string(b))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenCertficateRefreshSetsBody(t *testing.T) {
|
||||
sptCert := newServicePrincipalTokenCertificate(t)
|
||||
testServicePrincipalTokenRefreshSetsBody(t, sptCert, func(t *testing.T, b []byte) {
|
||||
body := string(b)
|
||||
|
||||
values, _ := url.ParseQuery(body)
|
||||
if values["client_assertion_type"][0] != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ||
|
||||
values["client_id"][0] != "id" ||
|
||||
values["grant_type"][0] != "client_credentials" ||
|
||||
values["resource"][0] != "resource" {
|
||||
t.Fatalf("adal: ServicePrincipalTokenCertificate#Refresh did not correctly set the HTTP Request Body.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenSecretRefreshSetsBody(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
testServicePrincipalTokenRefreshSetsBody(t, spt, func(t *testing.T, b []byte) {
|
||||
if string(b) != defaultFormData {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh did not correctly set the HTTP Request Body -- expected %v, received %v",
|
||||
defaultFormData, string(b))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshClosesRequestBody(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
if resp.Body.(*mocks.Body).IsOpen() {
|
||||
t.Fatal("adal: ServicePrincipalToken#Refresh failed to close the HTTP Response Body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshRejectsResponsesWithStatusNotOK(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusUnauthorized, "Unauthorized")
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err == nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh should reject a response with status != %d", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshRejectsEmptyBody(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return mocks.NewResponse(), nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.Refresh()
|
||||
if err == nil {
|
||||
t.Fatal("adal: ServicePrincipalToken#Refresh should reject an empty token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshPropagatesErrors(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
c := mocks.NewSender()
|
||||
c.SetError(fmt.Errorf("Faux Error"))
|
||||
spt.SetSender(c)
|
||||
|
||||
err := spt.Refresh()
|
||||
if err == nil {
|
||||
t.Fatal("adal: Failed to propagate the request error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshReturnsErrorIfNotOk(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
c := mocks.NewSender()
|
||||
c.AppendResponse(mocks.NewResponseWithStatus("401 NotAuthorized", http.StatusUnauthorized))
|
||||
spt.SetSender(c)
|
||||
|
||||
err := spt.Refresh()
|
||||
if err == nil {
|
||||
t.Fatalf("adal: Failed to return an when receiving a status code other than HTTP %d", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenRefreshUnmarshals(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
|
||||
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))
|
||||
j := newTokenJSON(expiresOn, "resource")
|
||||
resp := mocks.NewResponseWithContent(j)
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
} else if spt.AccessToken != "accessToken" ||
|
||||
spt.ExpiresIn != "3600" ||
|
||||
spt.ExpiresOn != expiresOn ||
|
||||
spt.NotBefore != expiresOn ||
|
||||
spt.Resource != "resource" ||
|
||||
spt.Type != "Bearer" {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh failed correctly unmarshal the JSON -- expected %v, received %v",
|
||||
j, *spt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenEnsureFreshRefreshes(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
expireToken(&spt.Token)
|
||||
|
||||
body := mocks.NewBody(newTokenJSON("test", "test"))
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
|
||||
f := false
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
f = true
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.EnsureFresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#EnsureFresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
if !f {
|
||||
t.Fatal("adal: ServicePrincipalToken#EnsureFresh failed to call Refresh for stale token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenEnsureFreshSkipsIfFresh(t *testing.T) {
|
||||
spt := newServicePrincipalToken()
|
||||
setTokenToExpireIn(&spt.Token, 1000*time.Second)
|
||||
|
||||
f := false
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
f = true
|
||||
return mocks.NewResponse(), nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
err := spt.EnsureFresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#EnsureFresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
if f {
|
||||
t.Fatal("adal: ServicePrincipalToken#EnsureFresh invoked Refresh for fresh token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCallback(t *testing.T) {
|
||||
callbackTriggered := false
|
||||
spt := newServicePrincipalToken(func(Token) error {
|
||||
callbackTriggered = true
|
||||
return nil
|
||||
})
|
||||
|
||||
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))
|
||||
|
||||
sender := mocks.NewSender()
|
||||
j := newTokenJSON(expiresOn, "resource")
|
||||
sender.AppendResponse(mocks.NewResponseWithContent(j))
|
||||
spt.SetSender(sender)
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err)
|
||||
}
|
||||
if !callbackTriggered {
|
||||
t.Fatalf("adal: RefreshCallback failed to trigger call callback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCallbackErrorPropagates(t *testing.T) {
|
||||
errorText := "this is an error text"
|
||||
spt := newServicePrincipalToken(func(Token) error {
|
||||
return fmt.Errorf(errorText)
|
||||
})
|
||||
|
||||
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))
|
||||
|
||||
sender := mocks.NewSender()
|
||||
j := newTokenJSON(expiresOn, "resource")
|
||||
sender.AppendResponse(mocks.NewResponseWithContent(j))
|
||||
spt.SetSender(sender)
|
||||
err := spt.Refresh()
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), errorText) {
|
||||
t.Fatalf("adal: RefreshCallback failed to propagate error")
|
||||
}
|
||||
}
|
||||
|
||||
// This demonstrates the danger of manual token without a refresh token
|
||||
func TestServicePrincipalTokenManualRefreshFailsWithoutRefresh(t *testing.T) {
|
||||
spt := newServicePrincipalTokenManual()
|
||||
spt.RefreshToken = ""
|
||||
err := spt.Refresh()
|
||||
if err == nil {
|
||||
t.Fatalf("adal: ServicePrincipalToken#Refresh should have failed with a ManualTokenSecret without a refresh token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServicePrincipalTokenFromMSI(t *testing.T) {
|
||||
resource := "https://resource"
|
||||
cb := func(token Token) error { return nil }
|
||||
|
||||
spt, err := NewServicePrincipalTokenFromMSI("http://msiendpoint/", resource, cb)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get MSI SPT: %v", err)
|
||||
}
|
||||
|
||||
// check some of the SPT fields
|
||||
if _, ok := spt.secret.(*ServicePrincipalMSISecret); !ok {
|
||||
t.Fatal("SPT secret was not of MSI type")
|
||||
}
|
||||
|
||||
if spt.resource != resource {
|
||||
t.Fatal("SPT came back with incorrect resource")
|
||||
}
|
||||
|
||||
if len(spt.refreshCallbacks) != 1 {
|
||||
t.Fatal("SPT had incorrect refresh callbacks.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVMEndpoint(t *testing.T) {
|
||||
tempSettingsFile, err := ioutil.TempFile("", "ManagedIdentity-Settings")
|
||||
if err != nil {
|
||||
t.Fatal("Couldn't write temp settings file")
|
||||
}
|
||||
defer os.Remove(tempSettingsFile.Name())
|
||||
|
||||
settingsContents := []byte(`{
|
||||
"url": "http://msiendpoint/"
|
||||
}`)
|
||||
|
||||
if _, err := tempSettingsFile.Write(settingsContents); err != nil {
|
||||
t.Fatal("Couldn't fill temp settings file")
|
||||
}
|
||||
|
||||
endpoint, err := getMSIVMEndpoint(tempSettingsFile.Name())
|
||||
if err != nil {
|
||||
t.Fatal("Coudn't get VM endpoint")
|
||||
}
|
||||
|
||||
if endpoint != "http://msiendpoint/" {
|
||||
t.Fatal("Didn't get correct endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
func newToken() *Token {
|
||||
return &Token{
|
||||
AccessToken: "ASECRETVALUE",
|
||||
Resource: "https://azure.microsoft.com/",
|
||||
Type: "Bearer",
|
||||
}
|
||||
}
|
||||
|
||||
func newTokenJSON(expiresOn string, resource string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"access_token" : "accessToken",
|
||||
"expires_in" : "3600",
|
||||
"expires_on" : "%s",
|
||||
"not_before" : "%s",
|
||||
"resource" : "%s",
|
||||
"token_type" : "Bearer"
|
||||
}`,
|
||||
expiresOn, expiresOn, resource)
|
||||
}
|
||||
|
||||
func newTokenExpiresIn(expireIn time.Duration) *Token {
|
||||
return setTokenToExpireIn(newToken(), expireIn)
|
||||
}
|
||||
|
||||
func newTokenExpiresAt(expireAt time.Time) *Token {
|
||||
return setTokenToExpireAt(newToken(), expireAt)
|
||||
}
|
||||
|
||||
func expireToken(t *Token) *Token {
|
||||
return setTokenToExpireIn(t, 0)
|
||||
}
|
||||
|
||||
func setTokenToExpireAt(t *Token, expireAt time.Time) *Token {
|
||||
t.ExpiresIn = "3600"
|
||||
t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(date.UnixEpoch()).Seconds()))
|
||||
t.NotBefore = t.ExpiresOn
|
||||
return t
|
||||
}
|
||||
|
||||
func setTokenToExpireIn(t *Token, expireIn time.Duration) *Token {
|
||||
return setTokenToExpireAt(t, time.Now().Add(expireIn))
|
||||
}
|
||||
|
||||
func newServicePrincipalToken(callbacks ...TokenRefreshCallback) *ServicePrincipalToken {
|
||||
spt, _ := NewServicePrincipalToken(TestOAuthConfig, "id", "secret", "resource", callbacks...)
|
||||
return spt
|
||||
}
|
||||
|
||||
func newServicePrincipalTokenManual() *ServicePrincipalToken {
|
||||
token := newToken()
|
||||
token.RefreshToken = "refreshtoken"
|
||||
spt, _ := NewServicePrincipalTokenFromManualToken(TestOAuthConfig, "id", "resource", *token)
|
||||
return spt
|
||||
}
|
||||
|
||||
func newServicePrincipalTokenCertificate(t *testing.T) *ServicePrincipalToken {
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{CommonName: "test"},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certificate, err := x509.ParseCertificate(certificateBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
spt, _ := NewServicePrincipalTokenFromCertificate(TestOAuthConfig, "id", certificate, privateKey, "resource")
|
||||
return spt
|
||||
}
|
||||
181
vendor/github.com/Azure/go-autorest/autorest/authorization.go
generated
vendored
Normal file
181
vendor/github.com/Azure/go-autorest/autorest/authorization.go
generated
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
package autorest
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
)
|
||||
|
||||
const (
|
||||
bearerChallengeHeader = "Www-Authenticate"
|
||||
bearer = "Bearer"
|
||||
tenantID = "tenantID"
|
||||
)
|
||||
|
||||
// Authorizer is the interface that provides a PrepareDecorator used to supply request
|
||||
// authorization. Most often, the Authorizer decorator runs last so it has access to the full
|
||||
// state of the formed HTTP request.
|
||||
type Authorizer interface {
|
||||
WithAuthorization() PrepareDecorator
|
||||
}
|
||||
|
||||
// NullAuthorizer implements a default, "do nothing" Authorizer.
|
||||
type NullAuthorizer struct{}
|
||||
|
||||
// WithAuthorization returns a PrepareDecorator that does nothing.
|
||||
func (na NullAuthorizer) WithAuthorization() PrepareDecorator {
|
||||
return WithNothing()
|
||||
}
|
||||
|
||||
// BearerAuthorizer implements the bearer authorization
|
||||
type BearerAuthorizer struct {
|
||||
tokenProvider adal.OAuthTokenProvider
|
||||
}
|
||||
|
||||
// NewBearerAuthorizer crates a BearerAuthorizer using the given token provider
|
||||
func NewBearerAuthorizer(tp adal.OAuthTokenProvider) *BearerAuthorizer {
|
||||
return &BearerAuthorizer{tokenProvider: tp}
|
||||
}
|
||||
|
||||
func (ba *BearerAuthorizer) withBearerAuthorization() PrepareDecorator {
|
||||
return WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", ba.tokenProvider.OAuthToken()))
|
||||
}
|
||||
|
||||
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
|
||||
// value is "Bearer " followed by the token.
|
||||
//
|
||||
// By default, the token will be automatically refreshed through the Refresher interface.
|
||||
func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator {
|
||||
return func(p Preparer) Preparer {
|
||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
refresher, ok := ba.tokenProvider.(adal.Refresher)
|
||||
if ok {
|
||||
err := refresher.EnsureFresh()
|
||||
if err != nil {
|
||||
return r, NewErrorWithError(err, "azure.BearerAuthorizer", "WithAuthorization", nil,
|
||||
"Failed to refresh the Token for request to %s", r.URL)
|
||||
}
|
||||
}
|
||||
return (ba.withBearerAuthorization()(p)).Prepare(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BearerAuthorizerCallbackFunc is the authentication callback signature.
|
||||
type BearerAuthorizerCallbackFunc func(tenantID, resource string) (*BearerAuthorizer, error)
|
||||
|
||||
// BearerAuthorizerCallback implements bearer authorization via a callback.
|
||||
type BearerAuthorizerCallback struct {
|
||||
sender Sender
|
||||
callback BearerAuthorizerCallbackFunc
|
||||
}
|
||||
|
||||
// NewBearerAuthorizerCallback creates a bearer authorization callback. The callback
|
||||
// is invoked when the HTTP request is submitted.
|
||||
func NewBearerAuthorizerCallback(sender Sender, callback BearerAuthorizerCallbackFunc) *BearerAuthorizerCallback {
|
||||
if sender == nil {
|
||||
sender = &http.Client{}
|
||||
}
|
||||
return &BearerAuthorizerCallback{sender: sender, callback: callback}
|
||||
}
|
||||
|
||||
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose value
|
||||
// is "Bearer " followed by the token. The BearerAuthorizer is obtained via a user-supplied callback.
|
||||
//
|
||||
// By default, the token will be automatically refreshed through the Refresher interface.
|
||||
func (bacb *BearerAuthorizerCallback) WithAuthorization() PrepareDecorator {
|
||||
return func(p Preparer) Preparer {
|
||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
// make a copy of the request and remove the body as it's not
|
||||
// required and avoids us having to create a copy of it.
|
||||
rCopy := *r
|
||||
removeRequestBody(&rCopy)
|
||||
|
||||
resp, err := bacb.sender.Do(&rCopy)
|
||||
if err == nil && resp.StatusCode == 401 {
|
||||
defer resp.Body.Close()
|
||||
if hasBearerChallenge(resp) {
|
||||
bc, err := newBearerChallenge(resp)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if bacb.callback != nil {
|
||||
ba, err := bacb.callback(bc.values[tenantID], bc.values["resource"])
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
return ba.WithAuthorization()(p).Prepare(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// returns true if the HTTP response contains a bearer challenge
|
||||
func hasBearerChallenge(resp *http.Response) bool {
|
||||
authHeader := resp.Header.Get(bearerChallengeHeader)
|
||||
if len(authHeader) == 0 || strings.Index(authHeader, bearer) < 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type bearerChallenge struct {
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
func newBearerChallenge(resp *http.Response) (bc bearerChallenge, err error) {
|
||||
challenge := strings.TrimSpace(resp.Header.Get(bearerChallengeHeader))
|
||||
trimmedChallenge := challenge[len(bearer)+1:]
|
||||
|
||||
// challenge is a set of key=value pairs that are comma delimited
|
||||
pairs := strings.Split(trimmedChallenge, ",")
|
||||
if len(pairs) < 1 {
|
||||
err = fmt.Errorf("challenge '%s' contains no pairs", challenge)
|
||||
return bc, err
|
||||
}
|
||||
|
||||
bc.values = make(map[string]string)
|
||||
for i := range pairs {
|
||||
trimmedPair := strings.TrimSpace(pairs[i])
|
||||
pair := strings.Split(trimmedPair, "=")
|
||||
if len(pair) == 2 {
|
||||
// remove the enclosing quotes
|
||||
key := strings.Trim(pair[0], "\"")
|
||||
value := strings.Trim(pair[1], "\"")
|
||||
|
||||
switch key {
|
||||
case "authorization", "authorization_uri":
|
||||
// strip the tenant ID from the authorization URL
|
||||
asURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return bc, err
|
||||
}
|
||||
bc.values[tenantID] = asURL.Path[1:]
|
||||
default:
|
||||
bc.values[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bc, err
|
||||
}
|
||||
188
vendor/github.com/Azure/go-autorest/autorest/authorization_test.go
generated
vendored
Normal file
188
vendor/github.com/Azure/go-autorest/autorest/authorization_test.go
generated
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
package autorest
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
TestTenantID = "TestTenantID"
|
||||
TestActiveDirectoryEndpoint = "https://login/test.com/"
|
||||
)
|
||||
|
||||
func TestWithAuthorizer(t *testing.T) {
|
||||
r1 := mocks.NewRequest()
|
||||
|
||||
na := &NullAuthorizer{}
|
||||
r2, err := Prepare(r1,
|
||||
na.WithAuthorization())
|
||||
if err != nil {
|
||||
t.Fatalf("autorest: NullAuthorizer#WithAuthorization returned an unexpected error (%v)", err)
|
||||
} else if !reflect.DeepEqual(r1, r2) {
|
||||
t.Fatalf("autorest: NullAuthorizer#WithAuthorization modified the request -- received %v, expected %v", r2, r1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenWithAuthorization(t *testing.T) {
|
||||
token := &adal.Token{
|
||||
AccessToken: "TestToken",
|
||||
Resource: "https://azure.microsoft.com/",
|
||||
Type: "Bearer",
|
||||
}
|
||||
|
||||
ba := NewBearerAuthorizer(token)
|
||||
req, err := Prepare(&http.Request{}, ba.WithAuthorization())
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
} else if req.Header.Get(http.CanonicalHeaderKey("Authorization")) != fmt.Sprintf("Bearer %s", token.AccessToken) {
|
||||
t.Fatal("azure: BearerAuthorizer#WithAuthorization failed to set Authorization header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenWithAuthorizationNoRefresh(t *testing.T) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, "id", "secret", "resource", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
spt.SetAutoRefresh(false)
|
||||
s := mocks.NewSender()
|
||||
spt.SetSender(s)
|
||||
|
||||
ba := NewBearerAuthorizer(spt)
|
||||
req, err := Prepare(mocks.NewRequest(), ba.WithAuthorization())
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
} else if req.Header.Get(http.CanonicalHeaderKey("Authorization")) != fmt.Sprintf("Bearer %s", spt.AccessToken) {
|
||||
t.Fatal("azure: BearerAuthorizer#WithAuthorization failed to set Authorization header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenWithAuthorizationRefresh(t *testing.T) {
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
refreshed := false
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, "id", "secret", "resource", func(t adal.Token) error {
|
||||
refreshed = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
|
||||
jwt := `{
|
||||
"access_token" : "accessToken",
|
||||
"expires_in" : "3600",
|
||||
"expires_on" : "test",
|
||||
"not_before" : "test",
|
||||
"resource" : "test",
|
||||
"token_type" : "Bearer"
|
||||
}`
|
||||
body := mocks.NewBody(jwt)
|
||||
resp := mocks.NewResponseWithBodyAndStatus(body, http.StatusOK, "OK")
|
||||
c := mocks.NewSender()
|
||||
s := DecorateSender(c,
|
||||
(func() SendDecorator {
|
||||
return func(s Sender) Sender {
|
||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
})())
|
||||
spt.SetSender(s)
|
||||
|
||||
ba := NewBearerAuthorizer(spt)
|
||||
req, err := Prepare(mocks.NewRequest(), ba.WithAuthorization())
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
} else if req.Header.Get(http.CanonicalHeaderKey("Authorization")) != fmt.Sprintf("Bearer %s", spt.AccessToken) {
|
||||
t.Fatal("azure: BearerAuthorizer#WithAuthorization failed to set Authorization header")
|
||||
}
|
||||
|
||||
if !refreshed {
|
||||
t.Fatal("azure: BearerAuthorizer#WithAuthorization must refresh the token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePrincipalTokenWithAuthorizationReturnsErrorIfConnotRefresh(t *testing.T) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, "id", "secret", "resource", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: BearerAuthorizer#WithAuthorization returned an error (%v)", err)
|
||||
}
|
||||
|
||||
s := mocks.NewSender()
|
||||
s.AppendResponse(mocks.NewResponseWithStatus("400 Bad Request", http.StatusBadRequest))
|
||||
spt.SetSender(s)
|
||||
|
||||
ba := NewBearerAuthorizer(spt)
|
||||
_, err = Prepare(mocks.NewRequest(), ba.WithAuthorization())
|
||||
if err == nil {
|
||||
t.Fatal("azure: BearerAuthorizer#WithAuthorization failed to return an error when refresh fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerAuthorizerCallback(t *testing.T) {
|
||||
tenantString := "123-tenantID-456"
|
||||
resourceString := "https://fake.resource.net"
|
||||
|
||||
s := mocks.NewSender()
|
||||
resp := mocks.NewResponseWithStatus("401 Unauthorized", http.StatusUnauthorized)
|
||||
mocks.SetResponseHeader(resp, bearerChallengeHeader, bearer+" \"authorization\"=\"https://fake.net/"+tenantString+"\",\"resource\"=\""+resourceString+"\"")
|
||||
s.AppendResponse(resp)
|
||||
|
||||
auth := NewBearerAuthorizerCallback(s, func(tenantID, resource string) (*BearerAuthorizer, error) {
|
||||
if tenantID != tenantString {
|
||||
t.Fatal("BearerAuthorizerCallback: bad tenant ID")
|
||||
}
|
||||
if resource != resourceString {
|
||||
t.Fatal("BearerAuthorizerCallback: bad resource")
|
||||
}
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfig(TestActiveDirectoryEndpoint, tenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: NewOAuthConfig returned an error (%v)", err)
|
||||
}
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, "id", "secret", resource)
|
||||
if err != nil {
|
||||
t.Fatalf("azure: NewServicePrincipalToken returned an error (%v)", err)
|
||||
}
|
||||
|
||||
spt.SetSender(s)
|
||||
return NewBearerAuthorizer(spt), nil
|
||||
})
|
||||
|
||||
_, err := Prepare(mocks.NewRequest(), auth.WithAuthorization())
|
||||
if err == nil {
|
||||
t.Fatal("azure: BearerAuthorizerCallback#WithAuthorization failed to return an error when refresh fails")
|
||||
}
|
||||
}
|
||||
132
vendor/github.com/Azure/go-autorest/autorest/autorest.go
generated
vendored
Normal file
132
vendor/github.com/Azure/go-autorest/autorest/autorest.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
Package autorest implements an HTTP request pipeline suitable for use across multiple go-routines
|
||||
and provides the shared routines relied on by AutoRest (see https://github.com/Azure/autorest/)
|
||||
generated Go code.
|
||||
|
||||
The package breaks sending and responding to HTTP requests into three phases: Preparing, Sending,
|
||||
and Responding. A typical pattern is:
|
||||
|
||||
req, err := Prepare(&http.Request{},
|
||||
token.WithAuthorization())
|
||||
|
||||
resp, err := Send(req,
|
||||
WithLogging(logger),
|
||||
DoErrorIfStatusCode(http.StatusInternalServerError),
|
||||
DoCloseIfError(),
|
||||
DoRetryForAttempts(5, time.Second))
|
||||
|
||||
err = Respond(resp,
|
||||
ByDiscardingBody(),
|
||||
ByClosing())
|
||||
|
||||
Each phase relies on decorators to modify and / or manage processing. Decorators may first modify
|
||||
and then pass the data along, pass the data first and then modify the result, or wrap themselves
|
||||
around passing the data (such as a logger might do). Decorators run in the order provided. For
|
||||
example, the following:
|
||||
|
||||
req, err := Prepare(&http.Request{},
|
||||
WithBaseURL("https://microsoft.com/"),
|
||||
WithPath("a"),
|
||||
WithPath("b"),
|
||||
WithPath("c"))
|
||||
|
||||
will set the URL to:
|
||||
|
||||
https://microsoft.com/a/b/c
|
||||
|
||||
Preparers and Responders may be shared and re-used (assuming the underlying decorators support
|
||||
sharing and re-use). Performant use is obtained by creating one or more Preparers and Responders
|
||||
shared among multiple go-routines, and a single Sender shared among multiple sending go-routines,
|
||||
all bound together by means of input / output channels.
|
||||
|
||||
Decorators hold their passed state within a closure (such as the path components in the example
|
||||
above). Be careful to share Preparers and Responders only in a context where such held state
|
||||
applies. For example, it may not make sense to share a Preparer that applies a query string from a
|
||||
fixed set of values. Similarly, sharing a Responder that reads the response body into a passed
|
||||
struct (e.g., ByUnmarshallingJson) is likely incorrect.
|
||||
|
||||
Lastly, the Swagger specification (https://swagger.io) that drives AutoRest
|
||||
(https://github.com/Azure/autorest/) precisely defines two date forms: date and date-time. The
|
||||
github.com/Azure/go-autorest/autorest/date package provides time.Time derivations to ensure
|
||||
correct parsing and formatting.
|
||||
|
||||
Errors raised by autorest objects and methods will conform to the autorest.Error interface.
|
||||
|
||||
See the included examples for more detail. For details on the suggested use of this package by
|
||||
generated clients, see the Client described below.
|
||||
*/
|
||||
package autorest
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderLocation specifies the HTTP Location header.
|
||||
HeaderLocation = "Location"
|
||||
|
||||
// HeaderRetryAfter specifies the HTTP Retry-After header.
|
||||
HeaderRetryAfter = "Retry-After"
|
||||
)
|
||||
|
||||
// ResponseHasStatusCode returns true if the status code in the HTTP Response is in the passed set
|
||||
// and false otherwise.
|
||||
func ResponseHasStatusCode(resp *http.Response, codes ...int) bool {
|
||||
if resp == nil {
|
||||
return false
|
||||
}
|
||||
return containsInt(codes, resp.StatusCode)
|
||||
}
|
||||
|
||||
// GetLocation retrieves the URL from the Location header of the passed response.
|
||||
func GetLocation(resp *http.Response) string {
|
||||
return resp.Header.Get(HeaderLocation)
|
||||
}
|
||||
|
||||
// GetRetryAfter extracts the retry delay from the Retry-After header of the passed response. If
|
||||
// the header is absent or is malformed, it will return the supplied default delay time.Duration.
|
||||
func GetRetryAfter(resp *http.Response, defaultDelay time.Duration) time.Duration {
|
||||
retry := resp.Header.Get(HeaderRetryAfter)
|
||||
if retry == "" {
|
||||
return defaultDelay
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(retry + "s")
|
||||
if err != nil {
|
||||
return defaultDelay
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// NewPollingRequest allocates and returns a new http.Request to poll for the passed response.
|
||||
func NewPollingRequest(resp *http.Response, cancel <-chan struct{}) (*http.Request, error) {
|
||||
location := GetLocation(resp)
|
||||
if location == "" {
|
||||
return nil, NewErrorWithResponse("autorest", "NewPollingRequest", resp, "Location header missing from response that requires polling")
|
||||
}
|
||||
|
||||
req, err := Prepare(&http.Request{Cancel: cancel},
|
||||
AsGet(),
|
||||
WithBaseURL(location))
|
||||
if err != nil {
|
||||
return nil, NewErrorWithError(err, "autorest", "NewPollingRequest", nil, "Failure creating poll request to %s", location)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
140
vendor/github.com/Azure/go-autorest/autorest/autorest_test.go
generated
vendored
Normal file
140
vendor/github.com/Azure/go-autorest/autorest/autorest_test.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
package autorest
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/mocks"
|
||||
)
|
||||
|
||||
func TestResponseHasStatusCode(t *testing.T) {
|
||||
codes := []int{http.StatusOK, http.StatusAccepted}
|
||||
resp := &http.Response{StatusCode: http.StatusAccepted}
|
||||
if !ResponseHasStatusCode(resp, codes...) {
|
||||
t.Fatalf("autorest: ResponseHasStatusCode failed to find %v in %v", resp.StatusCode, codes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseHasStatusCodeNotPresent(t *testing.T) {
|
||||
codes := []int{http.StatusOK, http.StatusAccepted}
|
||||
resp := &http.Response{StatusCode: http.StatusInternalServerError}
|
||||
if ResponseHasStatusCode(resp, codes...) {
|
||||
t.Fatalf("autorest: ResponseHasStatusCode unexpectedly found %v in %v", resp.StatusCode, codes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPollingRequestDoesNotReturnARequestWhenLocationHeaderIsMissing(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("500 InternalServerError", http.StatusInternalServerError)
|
||||
|
||||
req, _ := NewPollingRequest(resp, nil)
|
||||
if req != nil {
|
||||
t.Fatal("autorest: NewPollingRequest returned an http.Request when the Location header was missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPollingRequestReturnsAnErrorWhenPrepareFails(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
resp.Header.Set(http.CanonicalHeaderKey(HeaderLocation), mocks.TestBadURL)
|
||||
|
||||
_, err := NewPollingRequest(resp, nil)
|
||||
if err == nil {
|
||||
t.Fatal("autorest: NewPollingRequest failed to return an error when Prepare fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPollingRequestDoesNotReturnARequestWhenPrepareFails(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
resp.Header.Set(http.CanonicalHeaderKey(HeaderLocation), mocks.TestBadURL)
|
||||
|
||||
req, _ := NewPollingRequest(resp, nil)
|
||||
if req != nil {
|
||||
t.Fatal("autorest: NewPollingRequest returned an http.Request when Prepare failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPollingRequestReturnsAGetRequest(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
|
||||
req, _ := NewPollingRequest(resp, nil)
|
||||
if req.Method != "GET" {
|
||||
t.Fatalf("autorest: NewPollingRequest did not create an HTTP GET request -- actual method %v", req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPollingRequestProvidesTheURL(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
|
||||
req, _ := NewPollingRequest(resp, nil)
|
||||
if req.URL.String() != mocks.TestURL {
|
||||
t.Fatalf("autorest: NewPollingRequest did not create an HTTP with the expected URL -- received %v, expected %v", req.URL, mocks.TestURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLocation(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
|
||||
l := GetLocation(resp)
|
||||
if len(l) == 0 {
|
||||
t.Fatalf("autorest: GetLocation failed to return Location header -- expected %v, received %v", mocks.TestURL, l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLocationReturnsEmptyStringForMissingLocation(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
|
||||
l := GetLocation(resp)
|
||||
if len(l) != 0 {
|
||||
t.Fatalf("autorest: GetLocation return a value without a Location header -- received %v", l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRetryAfter(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
|
||||
d := GetRetryAfter(resp, DefaultPollingDelay)
|
||||
if d != mocks.TestDelay {
|
||||
t.Fatalf("autorest: GetRetryAfter failed to returned the expected delay -- expected %v, received %v", mocks.TestDelay, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRetryAfterReturnsDefaultDelayIfRetryHeaderIsMissing(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
|
||||
d := GetRetryAfter(resp, DefaultPollingDelay)
|
||||
if d != DefaultPollingDelay {
|
||||
t.Fatalf("autorest: GetRetryAfter failed to returned the default delay for a missing Retry-After header -- expected %v, received %v",
|
||||
DefaultPollingDelay, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRetryAfterReturnsDefaultDelayIfRetryHeaderIsMalformed(t *testing.T) {
|
||||
resp := mocks.NewResponseWithStatus("202 Accepted", http.StatusAccepted)
|
||||
mocks.SetAcceptedHeaders(resp)
|
||||
resp.Header.Set(http.CanonicalHeaderKey(HeaderRetryAfter), "a very bad non-integer value")
|
||||
|
||||
d := GetRetryAfter(resp, DefaultPollingDelay)
|
||||
if d != DefaultPollingDelay {
|
||||
t.Fatalf("autorest: GetRetryAfter failed to returned the default delay for a malformed Retry-After header -- expected %v, received %v",
|
||||
DefaultPollingDelay, d)
|
||||
}
|
||||
}
|
||||
460
vendor/github.com/Azure/go-autorest/autorest/azure/async.go
generated
vendored
Normal file
460
vendor/github.com/Azure/go-autorest/autorest/azure/async.go
generated
vendored
Normal file
@@ -0,0 +1,460 @@
|
||||
package azure
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/date"
|
||||
)
|
||||
|
||||
const (
|
||||
headerAsyncOperation = "Azure-AsyncOperation"
|
||||
)
|
||||
|
||||
const (
|
||||
operationInProgress string = "InProgress"
|
||||
operationCanceled string = "Canceled"
|
||||
operationFailed string = "Failed"
|
||||
operationSucceeded string = "Succeeded"
|
||||
)
|
||||
|
||||
var pollingCodes = [...]int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
|
||||
|
||||
// Future provides a mechanism to access the status and results of an asynchronous request.
|
||||
// Since futures are stateful they should be passed by value to avoid race conditions.
|
||||
type Future struct {
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
ps pollingState
|
||||
}
|
||||
|
||||
// NewFuture returns a new Future object initialized with the specified request.
|
||||
func NewFuture(req *http.Request) Future {
|
||||
return Future{req: req}
|
||||
}
|
||||
|
||||
// Response returns the last HTTP response or nil if there isn't one.
|
||||
func (f Future) Response() *http.Response {
|
||||
return f.resp
|
||||
}
|
||||
|
||||
// Status returns the last status message of the operation.
|
||||
func (f Future) Status() string {
|
||||
if f.ps.State == "" {
|
||||
return "Unknown"
|
||||
}
|
||||
return f.ps.State
|
||||
}
|
||||
|
||||
// PollingMethod returns the method used to monitor the status of the asynchronous operation.
|
||||
func (f Future) PollingMethod() PollingMethodType {
|
||||
return f.ps.PollingMethod
|
||||
}
|
||||
|
||||
// Done queries the service to see if the operation has completed.
|
||||
func (f *Future) Done(sender autorest.Sender) (bool, error) {
|
||||
// exit early if this future has terminated
|
||||
if f.ps.hasTerminated() {
|
||||
return true, f.errorInfo()
|
||||
}
|
||||
|
||||
resp, err := sender.Do(f.req)
|
||||
f.resp = resp
|
||||
if err != nil || !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = updatePollingState(resp, &f.ps)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if f.ps.hasTerminated() {
|
||||
return true, f.errorInfo()
|
||||
}
|
||||
|
||||
f.req, err = newPollingRequest(f.ps)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// GetPollingDelay returns a duration the application should wait before checking
|
||||
// the status of the asynchronous request and true; this value is returned from
|
||||
// the service via the Retry-After response header. If the header wasn't returned
|
||||
// then the function returns the zero-value time.Duration and false.
|
||||
func (f Future) GetPollingDelay() (time.Duration, bool) {
|
||||
if f.resp == nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
retry := f.resp.Header.Get(autorest.HeaderRetryAfter)
|
||||
if retry == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(retry + "s")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return d, true
|
||||
}
|
||||
|
||||
// WaitForCompletion will return when one of the following conditions is met: the long
|
||||
// running operation has completed, the provided context is cancelled, or the client's
|
||||
// polling duration has been exceeded. It will retry failed polling attempts based on
|
||||
// the retry value defined in the client up to the maximum retry attempts.
|
||||
func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.PollingDuration)
|
||||
defer cancel()
|
||||
|
||||
done, err := f.Done(client)
|
||||
for attempts := 0; !done; done, err = f.Done(client) {
|
||||
if attempts >= client.RetryAttempts {
|
||||
return autorest.NewErrorWithError(err, "azure", "WaitForCompletion", f.resp, "the number of retries has been exceeded")
|
||||
}
|
||||
// we want delayAttempt to be zero in the non-error case so
|
||||
// that DelayForBackoff doesn't perform exponential back-off
|
||||
var delayAttempt int
|
||||
var delay time.Duration
|
||||
if err == nil {
|
||||
// check for Retry-After delay, if not present use the client's polling delay
|
||||
var ok bool
|
||||
delay, ok = f.GetPollingDelay()
|
||||
if !ok {
|
||||
delay = client.PollingDelay
|
||||
}
|
||||
} else {
|
||||
// there was an error polling for status so perform exponential
|
||||
// back-off based on the number of attempts using the client's retry
|
||||
// duration. update attempts after delayAttempt to avoid off-by-one.
|
||||
delayAttempt = attempts
|
||||
delay = client.RetryDuration
|
||||
attempts++
|
||||
}
|
||||
// wait until the delay elapses or the context is cancelled
|
||||
delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, ctx.Done())
|
||||
if !delayElapsed {
|
||||
return autorest.NewErrorWithError(ctx.Err(), "azure", "WaitForCompletion", f.resp, "context has been cancelled")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// if the operation failed the polling state will contain
|
||||
// error information and implements the error interface
|
||||
func (f *Future) errorInfo() error {
|
||||
if !f.ps.hasSucceeded() {
|
||||
return f.ps
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (f Future) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&f.ps)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (f *Future) UnmarshalJSON(data []byte) error {
|
||||
err := json.Unmarshal(data, &f.ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.req, err = newPollingRequest(f.ps)
|
||||
return err
|
||||
}
|
||||
|
||||
// DoPollForAsynchronous returns a SendDecorator that polls if the http.Response is for an Azure
|
||||
// long-running operation. It will delay between requests for the duration specified in the
|
||||
// RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled by
|
||||
// closing the optional channel on the http.Request.
|
||||
func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
|
||||
return func(s autorest.Sender) autorest.Sender {
|
||||
return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||
resp, err = s.Do(r)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
ps := pollingState{}
|
||||
for err == nil {
|
||||
err = updatePollingState(resp, &ps)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if ps.hasTerminated() {
|
||||
if !ps.hasSucceeded() {
|
||||
err = ps
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
r, err = newPollingRequest(ps)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
r.Cancel = resp.Request.Cancel
|
||||
|
||||
delay = autorest.GetRetryAfter(resp, delay)
|
||||
resp, err = autorest.SendWithSender(s, r,
|
||||
autorest.AfterDelay(delay))
|
||||
}
|
||||
|
||||
return resp, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getAsyncOperation(resp *http.Response) string {
|
||||
return resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
|
||||
}
|
||||
|
||||
func hasSucceeded(state string) bool {
|
||||
return strings.EqualFold(state, operationSucceeded)
|
||||
}
|
||||
|
||||
func hasTerminated(state string) bool {
|
||||
return strings.EqualFold(state, operationCanceled) || strings.EqualFold(state, operationFailed) || strings.EqualFold(state, operationSucceeded)
|
||||
}
|
||||
|
||||
func hasFailed(state string) bool {
|
||||
return strings.EqualFold(state, operationFailed)
|
||||
}
|
||||
|
||||
type provisioningTracker interface {
|
||||
state() string
|
||||
hasSucceeded() bool
|
||||
hasTerminated() bool
|
||||
}
|
||||
|
||||
type operationResource struct {
|
||||
// Note:
|
||||
// The specification states services should return the "id" field. However some return it as
|
||||
// "operationId".
|
||||
ID string `json:"id"`
|
||||
OperationID string `json:"operationId"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Properties map[string]interface{} `json:"properties"`
|
||||
OperationError ServiceError `json:"error"`
|
||||
StartTime date.Time `json:"startTime"`
|
||||
EndTime date.Time `json:"endTime"`
|
||||
PercentComplete float64 `json:"percentComplete"`
|
||||
}
|
||||
|
||||
func (or operationResource) state() string {
|
||||
return or.Status
|
||||
}
|
||||
|
||||
func (or operationResource) hasSucceeded() bool {
|
||||
return hasSucceeded(or.state())
|
||||
}
|
||||
|
||||
func (or operationResource) hasTerminated() bool {
|
||||
return hasTerminated(or.state())
|
||||
}
|
||||
|
||||
type provisioningProperties struct {
|
||||
ProvisioningState string `json:"provisioningState"`
|
||||
}
|
||||
|
||||
type provisioningStatus struct {
|
||||
Properties provisioningProperties `json:"properties,omitempty"`
|
||||
ProvisioningError ServiceError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (ps provisioningStatus) state() string {
|
||||
return ps.Properties.ProvisioningState
|
||||
}
|
||||
|
||||
func (ps provisioningStatus) hasSucceeded() bool {
|
||||
return hasSucceeded(ps.state())
|
||||
}
|
||||
|
||||
func (ps provisioningStatus) hasTerminated() bool {
|
||||
return hasTerminated(ps.state())
|
||||
}
|
||||
|
||||
func (ps provisioningStatus) hasProvisioningError() bool {
|
||||
return ps.ProvisioningError != ServiceError{}
|
||||
}
|
||||
|
||||
// PollingMethodType defines a type used for enumerating polling mechanisms.
|
||||
type PollingMethodType string
|
||||
|
||||
const (
|
||||
// PollingAsyncOperation indicates the polling method uses the Azure-AsyncOperation header.
|
||||
PollingAsyncOperation PollingMethodType = "AsyncOperation"
|
||||
|
||||
// PollingLocation indicates the polling method uses the Location header.
|
||||
PollingLocation PollingMethodType = "Location"
|
||||
|
||||
// PollingUnknown indicates an unknown polling method and is the default value.
|
||||
PollingUnknown PollingMethodType = ""
|
||||
)
|
||||
|
||||
type pollingState struct {
|
||||
PollingMethod PollingMethodType `json:"pollingMethod"`
|
||||
URI string `json:"uri"`
|
||||
State string `json:"state"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (ps pollingState) hasSucceeded() bool {
|
||||
return hasSucceeded(ps.State)
|
||||
}
|
||||
|
||||
func (ps pollingState) hasTerminated() bool {
|
||||
return hasTerminated(ps.State)
|
||||
}
|
||||
|
||||
func (ps pollingState) hasFailed() bool {
|
||||
return hasFailed(ps.State)
|
||||
}
|
||||
|
||||
func (ps pollingState) Error() string {
|
||||
return fmt.Sprintf("Long running operation terminated with status '%s': Code=%q Message=%q", ps.State, ps.Code, ps.Message)
|
||||
}
|
||||
|
||||
// updatePollingState maps the operation status -- retrieved from either a provisioningState
|
||||
// field, the status field of an OperationResource, or inferred from the HTTP status code --
|
||||
// into a well-known states. Since the process begins from the initial request, the state
|
||||
// always comes from either a the provisioningState returned or is inferred from the HTTP
|
||||
// status code. Subsequent requests will read an Azure OperationResource object if the
|
||||
// service initially returned the Azure-AsyncOperation header. The responseFormat field notes
|
||||
// the expected response format.
|
||||
func updatePollingState(resp *http.Response, ps *pollingState) error {
|
||||
// Determine the response shape
|
||||
// -- The first response will always be a provisioningStatus response; only the polling requests,
|
||||
// depending on the header returned, may be something otherwise.
|
||||
var pt provisioningTracker
|
||||
if ps.PollingMethod == PollingAsyncOperation {
|
||||
pt = &operationResource{}
|
||||
} else {
|
||||
pt = &provisioningStatus{}
|
||||
}
|
||||
|
||||
// If this is the first request (that is, the polling response shape is unknown), determine how
|
||||
// to poll and what to expect
|
||||
if ps.PollingMethod == PollingUnknown {
|
||||
req := resp.Request
|
||||
if req == nil {
|
||||
return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Original HTTP request is missing")
|
||||
}
|
||||
|
||||
// Prefer the Azure-AsyncOperation header
|
||||
ps.URI = getAsyncOperation(resp)
|
||||
if ps.URI != "" {
|
||||
ps.PollingMethod = PollingAsyncOperation
|
||||
} else {
|
||||
ps.PollingMethod = PollingLocation
|
||||
}
|
||||
|
||||
// Else, use the Location header
|
||||
if ps.URI == "" {
|
||||
ps.URI = autorest.GetLocation(resp)
|
||||
}
|
||||
|
||||
// Lastly, requests against an existing resource, use the last request URI
|
||||
if ps.URI == "" {
|
||||
m := strings.ToUpper(req.Method)
|
||||
if m == http.MethodPatch || m == http.MethodPut || m == http.MethodGet {
|
||||
ps.URI = req.URL.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read and interpret the response (saving the Body in case no polling is necessary)
|
||||
b := &bytes.Buffer{}
|
||||
err := autorest.Respond(resp,
|
||||
autorest.ByCopying(b),
|
||||
autorest.ByUnmarshallingJSON(pt),
|
||||
autorest.ByClosing())
|
||||
resp.Body = ioutil.NopCloser(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Interpret the results
|
||||
// -- Terminal states apply regardless
|
||||
// -- Unknown states are per-service inprogress states
|
||||
// -- Otherwise, infer state from HTTP status code
|
||||
if pt.hasTerminated() {
|
||||
ps.State = pt.state()
|
||||
} else if pt.state() != "" {
|
||||
ps.State = operationInProgress
|
||||
} else {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusAccepted:
|
||||
ps.State = operationInProgress
|
||||
|
||||
case http.StatusNoContent, http.StatusCreated, http.StatusOK:
|
||||
ps.State = operationSucceeded
|
||||
|
||||
default:
|
||||
ps.State = operationFailed
|
||||
}
|
||||
}
|
||||
|
||||
if strings.EqualFold(ps.State, operationInProgress) && ps.URI == "" {
|
||||
return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Unable to obtain polling URI for %s %s", resp.Request.Method, resp.Request.URL)
|
||||
}
|
||||
|
||||
// For failed operation, check for error code and message in
|
||||
// -- Operation resource
|
||||
// -- Response
|
||||
// -- Otherwise, Unknown
|
||||
if ps.hasFailed() {
|
||||
if ps.PollingMethod == PollingAsyncOperation {
|
||||
or := pt.(*operationResource)
|
||||
ps.Code = or.OperationError.Code
|
||||
ps.Message = or.OperationError.Message
|
||||
} else {
|
||||
p := pt.(*provisioningStatus)
|
||||
if p.hasProvisioningError() {
|
||||
ps.Code = p.ProvisioningError.Code
|
||||
ps.Message = p.ProvisioningError.Message
|
||||
} else {
|
||||
ps.Code = "Unknown"
|
||||
ps.Message = "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPollingRequest(ps pollingState) (*http.Request, error) {
|
||||
reqPoll, err := autorest.Prepare(&http.Request{},
|
||||
autorest.AsGet(),
|
||||
autorest.WithBaseURL(ps.URI))
|
||||
if err != nil {
|
||||
return nil, autorest.NewErrorWithError(err, "azure", "newPollingRequest", nil, "Failure creating poll request to %s", ps.URI)
|
||||
}
|
||||
|
||||
return reqPoll, nil
|
||||
}
|
||||
1327
vendor/github.com/Azure/go-autorest/autorest/azure/async_test.go
generated
vendored
Normal file
1327
vendor/github.com/Azure/go-autorest/autorest/azure/async_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
143
vendor/github.com/Azure/go-autorest/autorest/azure/auth/authfile.go
generated
vendored
Normal file
143
vendor/github.com/Azure/go-autorest/autorest/azure/auth/authfile.go
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/dimchansky/utfbom"
|
||||
)
|
||||
|
||||
// ClientSetup includes authentication details and cloud specific
|
||||
// parameters for ARM clients
|
||||
type ClientSetup struct {
|
||||
*autorest.BearerAuthorizer
|
||||
File
|
||||
BaseURI string
|
||||
}
|
||||
|
||||
// File represents the authentication file
|
||||
type File struct {
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
ClientSecret string `json:"clientSecret,omitempty"`
|
||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpointUrl,omitempty"`
|
||||
GraphResourceID string `json:"activeDirectoryGraphResourceId,omitempty"`
|
||||
SQLManagementEndpoint string `json:"sqlManagementEndpointUrl,omitempty"`
|
||||
GalleryEndpoint string `json:"galleryEndpointUrl,omitempty"`
|
||||
ManagementEndpoint string `json:"managementEndpointUrl,omitempty"`
|
||||
}
|
||||
|
||||
// GetClientSetup provides an authorizer, base URI, subscriptionID and
|
||||
// tenantID parameters from an Azure CLI auth file
|
||||
func GetClientSetup(baseURI string) (auth ClientSetup, err error) {
|
||||
fileLocation := os.Getenv("AZURE_AUTH_LOCATION")
|
||||
if fileLocation == "" {
|
||||
return auth, errors.New("auth file not found. Environment variable AZURE_AUTH_LOCATION is not set")
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(fileLocation)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Auth file might be encoded
|
||||
decoded, err := decode(contents)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(decoded, &auth.File)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resource, err := getResourceForToken(auth.File, baseURI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
auth.BaseURI = resource
|
||||
|
||||
config, err := adal.NewOAuthConfig(auth.ActiveDirectoryEndpoint, auth.TenantID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
spToken, err := adal.NewServicePrincipalToken(*config, auth.ClientID, auth.ClientSecret, resource)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auth.BearerAuthorizer = autorest.NewBearerAuthorizer(spToken)
|
||||
return
|
||||
}
|
||||
|
||||
func decode(b []byte) ([]byte, error) {
|
||||
reader, enc := utfbom.Skip(bytes.NewReader(b))
|
||||
|
||||
switch enc {
|
||||
case utfbom.UTF16LittleEndian:
|
||||
u16 := make([]uint16, (len(b)/2)-1)
|
||||
err := binary.Read(reader, binary.LittleEndian, &u16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16))), nil
|
||||
case utfbom.UTF16BigEndian:
|
||||
u16 := make([]uint16, (len(b)/2)-1)
|
||||
err := binary.Read(reader, binary.BigEndian, &u16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16))), nil
|
||||
}
|
||||
return ioutil.ReadAll(reader)
|
||||
}
|
||||
|
||||
func getResourceForToken(f File, baseURI string) (string, error) {
|
||||
// Compare dafault base URI from the SDK to the endpoints from the public cloud
|
||||
// Base URI and token resource are the same string. This func finds the authentication
|
||||
// file field that matches the SDK base URI. The SDK defines the public cloud
|
||||
// endpoint as its default base URI
|
||||
if !strings.HasSuffix(baseURI, "/") {
|
||||
baseURI += "/"
|
||||
}
|
||||
switch baseURI {
|
||||
case azure.PublicCloud.ServiceManagementEndpoint:
|
||||
return f.ManagementEndpoint, nil
|
||||
case azure.PublicCloud.ResourceManagerEndpoint:
|
||||
return f.ResourceManagerEndpoint, nil
|
||||
case azure.PublicCloud.ActiveDirectoryEndpoint:
|
||||
return f.ActiveDirectoryEndpoint, nil
|
||||
case azure.PublicCloud.GalleryEndpoint:
|
||||
return f.GalleryEndpoint, nil
|
||||
case azure.PublicCloud.GraphEndpoint:
|
||||
return f.GraphResourceID, nil
|
||||
}
|
||||
return "", fmt.Errorf("auth: base URI not found in endpoints")
|
||||
}
|
||||
111
vendor/github.com/Azure/go-autorest/autorest/azure/auth/authfile_test.go
generated
vendored
Normal file
111
vendor/github.com/Azure/go-autorest/autorest/azure/auth/authfile_test.go
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
expectedFile = File{
|
||||
ClientID: "client-id-123",
|
||||
ClientSecret: "client-secret-456",
|
||||
SubscriptionID: "sub-id-789",
|
||||
TenantID: "tenant-id-123",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
|
||||
ResourceManagerEndpoint: "https://management.azure.com/",
|
||||
GraphResourceID: "https://graph.windows.net/",
|
||||
SQLManagementEndpoint: "https://management.core.windows.net:8443/",
|
||||
GalleryEndpoint: "https://gallery.azure.com/",
|
||||
ManagementEndpoint: "https://management.core.windows.net/",
|
||||
}
|
||||
)
|
||||
|
||||
func TestGetClientSetup(t *testing.T) {
|
||||
os.Setenv("AZURE_AUTH_LOCATION", filepath.Join(getCredsPath(), "credsutf16le.json"))
|
||||
setup, err := GetClientSetup("https://management.azure.com")
|
||||
if err != nil {
|
||||
t.Logf("GetClientSetup failed, got error %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if setup.BaseURI != "https://management.azure.com/" {
|
||||
t.Logf("auth.BaseURI not set correctly, expected 'https://management.azure.com/', got '%s'", setup.BaseURI)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedFile, setup.File) {
|
||||
t.Logf("auth.File not set correctly, expected %v, got %v", expectedFile, setup.File)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if setup.BearerAuthorizer == nil {
|
||||
t.Log("auth.Authorizer not set correctly, got nil")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeAndUnmarshal(t *testing.T) {
|
||||
tests := []string{
|
||||
"credsutf8.json",
|
||||
"credsutf16le.json",
|
||||
"credsutf16be.json",
|
||||
}
|
||||
creds := getCredsPath()
|
||||
for _, test := range tests {
|
||||
b, err := ioutil.ReadFile(filepath.Join(creds, test))
|
||||
if err != nil {
|
||||
t.Logf("error reading file '%s': %s", test, err)
|
||||
t.Fail()
|
||||
}
|
||||
decoded, err := decode(b)
|
||||
if err != nil {
|
||||
t.Logf("error decoding file '%s': %s", test, err)
|
||||
t.Fail()
|
||||
}
|
||||
var got File
|
||||
err = json.Unmarshal(decoded, &got)
|
||||
if err != nil {
|
||||
t.Logf("error unmarshaling file '%s': %s", test, err)
|
||||
t.Fail()
|
||||
}
|
||||
if !reflect.DeepEqual(expectedFile, got) {
|
||||
t.Logf("unmarshaled map expected %v, got %v", expectedFile, got)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getCredsPath() string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
return filepath.Join(gopath, "src", "github.com", "Azure", "go-autorest", "testdata")
|
||||
}
|
||||
|
||||
func areMapsEqual(a, b map[string]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k := range a {
|
||||
if a[k] != b[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
200
vendor/github.com/Azure/go-autorest/autorest/azure/azure.go
generated
vendored
Normal file
200
vendor/github.com/Azure/go-autorest/autorest/azure/azure.go
generated
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Package azure provides Azure-specific implementations used with AutoRest.
|
||||
|
||||
See the included examples for more detail.
|
||||
*/
|
||||
package azure
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderClientID is the Azure extension header to set a user-specified request ID.
|
||||
HeaderClientID = "x-ms-client-request-id"
|
||||
|
||||
// HeaderReturnClientID is the Azure extension header to set if the user-specified request ID
|
||||
// should be included in the response.
|
||||
HeaderReturnClientID = "x-ms-return-client-request-id"
|
||||
|
||||
// HeaderRequestID is the Azure extension header of the service generated request ID returned
|
||||
// in the response.
|
||||
HeaderRequestID = "x-ms-request-id"
|
||||
)
|
||||
|
||||
// ServiceError encapsulates the error response from an Azure service.
|
||||
type ServiceError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details *[]interface{} `json:"details"`
|
||||
}
|
||||
|
||||
func (se ServiceError) Error() string {
|
||||
if se.Details != nil {
|
||||
d, err := json.Marshal(*(se.Details))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Code=%q Message=%q Details=%v", se.Code, se.Message, *se.Details)
|
||||
}
|
||||
return fmt.Sprintf("Code=%q Message=%q Details=%v", se.Code, se.Message, string(d))
|
||||
}
|
||||
return fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message)
|
||||
}
|
||||
|
||||
// RequestError describes an error response returned by Azure service.
|
||||
type RequestError struct {
|
||||
autorest.DetailedError
|
||||
|
||||
// The error returned by the Azure service.
|
||||
ServiceError *ServiceError `json:"error"`
|
||||
|
||||
// The request id (from the x-ms-request-id-header) of the request.
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// Error returns a human-friendly error message from service error.
|
||||
func (e RequestError) Error() string {
|
||||
return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v",
|
||||
e.StatusCode, e.ServiceError)
|
||||
}
|
||||
|
||||
// IsAzureError returns true if the passed error is an Azure Service error; false otherwise.
|
||||
func IsAzureError(e error) bool {
|
||||
_, ok := e.(*RequestError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewErrorWithError creates a new Error conforming object from the
|
||||
// passed packageType, method, statusCode of the given resp (UndefinedStatusCode
|
||||
// if resp is nil), message, and original error. message is treated as a format
|
||||
// string to which the optional args apply.
|
||||
func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError {
|
||||
if v, ok := original.(*RequestError); ok {
|
||||
return *v
|
||||
}
|
||||
|
||||
statusCode := autorest.UndefinedStatusCode
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
return RequestError{
|
||||
DetailedError: autorest.DetailedError{
|
||||
Original: original,
|
||||
PackageType: packageType,
|
||||
Method: method,
|
||||
StatusCode: statusCode,
|
||||
Message: fmt.Sprintf(message, args...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||
// x-ms-client-request-id whose value is the passed, undecorated UUID (e.g.,
|
||||
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id
|
||||
// header to true such that UUID accompanies the http.Response.
|
||||
func WithReturningClientID(uuid string) autorest.PrepareDecorator {
|
||||
preparer := autorest.CreatePreparer(
|
||||
WithClientID(uuid),
|
||||
WithReturnClientID(true))
|
||||
|
||||
return func(p autorest.Preparer) autorest.Preparer {
|
||||
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
r, err := p.Prepare(r)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
return preparer.Prepare(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||
// x-ms-client-request-id whose value is passed, undecorated UUID (e.g.,
|
||||
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA").
|
||||
func WithClientID(uuid string) autorest.PrepareDecorator {
|
||||
return autorest.WithHeader(HeaderClientID, uuid)
|
||||
}
|
||||
|
||||
// WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||
// x-ms-return-client-request-id whose boolean value indicates if the value of the
|
||||
// x-ms-client-request-id header should be included in the http.Response.
|
||||
func WithReturnClientID(b bool) autorest.PrepareDecorator {
|
||||
return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b))
|
||||
}
|
||||
|
||||
// ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the
|
||||
// http.Request sent to the service (and returned in the http.Response)
|
||||
func ExtractClientID(resp *http.Response) string {
|
||||
return autorest.ExtractHeaderValue(HeaderClientID, resp)
|
||||
}
|
||||
|
||||
// ExtractRequestID extracts the Azure server generated request identifier from the
|
||||
// x-ms-request-id header.
|
||||
func ExtractRequestID(resp *http.Response) string {
|
||||
return autorest.ExtractHeaderValue(HeaderRequestID, resp)
|
||||
}
|
||||
|
||||
// WithErrorUnlessStatusCode returns a RespondDecorator that emits an
|
||||
// azure.RequestError by reading the response body unless the response HTTP status code
|
||||
// is among the set passed.
|
||||
//
|
||||
// If there is a chance service may return responses other than the Azure error
|
||||
// format and the response cannot be parsed into an error, a decoding error will
|
||||
// be returned containing the response body. In any case, the Responder will
|
||||
// return an error if the status code is not satisfied.
|
||||
//
|
||||
// If this Responder returns an error, the response body will be replaced with
|
||||
// an in-memory reader, which needs no further closing.
|
||||
func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
|
||||
return func(r autorest.Responder) autorest.Responder {
|
||||
return autorest.ResponderFunc(func(resp *http.Response) error {
|
||||
err := r.Respond(resp)
|
||||
if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) {
|
||||
var e RequestError
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy and replace the Body in case it does not contain an error object.
|
||||
// This will leave the Body available to the caller.
|
||||
b, decodeErr := autorest.CopyAndDecode(autorest.EncodedAsJSON, resp.Body, &e)
|
||||
resp.Body = ioutil.NopCloser(&b)
|
||||
if decodeErr != nil {
|
||||
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr)
|
||||
} else if e.ServiceError == nil {
|
||||
// Check if error is unwrapped ServiceError
|
||||
if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil || e.ServiceError.Message == "" {
|
||||
e.ServiceError = &ServiceError{
|
||||
Code: "Unknown",
|
||||
Message: "Unknown service error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.RequestID = ExtractRequestID(resp)
|
||||
if e.StatusCode == nil {
|
||||
e.StatusCode = resp.StatusCode
|
||||
}
|
||||
err = &e
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
513
vendor/github.com/Azure/go-autorest/autorest/azure/azure_test.go
generated
vendored
Normal file
513
vendor/github.com/Azure/go-autorest/autorest/azure/azure_test.go
generated
vendored
Normal file
@@ -0,0 +1,513 @@
|
||||
package azure
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
headerAuthorization = "Authorization"
|
||||
longDelay = 5 * time.Second
|
||||
retryDelay = 10 * time.Millisecond
|
||||
testLogPrefix = "azure:"
|
||||
)
|
||||
|
||||
// Use a Client Inspector to set the request identifier.
|
||||
func ExampleWithClientID() {
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
req, _ := autorest.Prepare(&http.Request{},
|
||||
autorest.AsGet(),
|
||||
autorest.WithBaseURL("https://microsoft.com/a/b/c/"))
|
||||
|
||||
c := autorest.Client{Sender: mocks.NewSender()}
|
||||
c.RequestInspector = WithReturningClientID(uuid)
|
||||
|
||||
autorest.SendWithSender(c, req)
|
||||
fmt.Printf("Inspector added the %s header with the value %s\n",
|
||||
HeaderClientID, req.Header.Get(HeaderClientID))
|
||||
fmt.Printf("Inspector added the %s header with the value %s\n",
|
||||
HeaderReturnClientID, req.Header.Get(HeaderReturnClientID))
|
||||
// Output:
|
||||
// Inspector added the x-ms-client-request-id header with the value 71FDB9F4-5E49-4C12-B266-DE7B4FD999A6
|
||||
// Inspector added the x-ms-return-client-request-id header with the value true
|
||||
}
|
||||
|
||||
func TestWithReturningClientIDReturnsError(t *testing.T) {
|
||||
var errIn error
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
_, errOut := autorest.Prepare(&http.Request{},
|
||||
withErrorPrepareDecorator(&errIn),
|
||||
WithReturningClientID(uuid))
|
||||
|
||||
if errOut == nil || errIn != errOut {
|
||||
t.Fatalf("azure: WithReturningClientID failed to exit early when receiving an error -- expected (%v), received (%v)",
|
||||
errIn, errOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithClientID(t *testing.T) {
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
req, _ := autorest.Prepare(&http.Request{},
|
||||
WithClientID(uuid))
|
||||
|
||||
if req.Header.Get(HeaderClientID) != uuid {
|
||||
t.Fatalf("azure: WithClientID failed to set %s -- expected %s, received %s",
|
||||
HeaderClientID, uuid, req.Header.Get(HeaderClientID))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithReturnClientID(t *testing.T) {
|
||||
b := false
|
||||
req, _ := autorest.Prepare(&http.Request{},
|
||||
WithReturnClientID(b))
|
||||
|
||||
if req.Header.Get(HeaderReturnClientID) != strconv.FormatBool(b) {
|
||||
t.Fatalf("azure: WithReturnClientID failed to set %s -- expected %s, received %s",
|
||||
HeaderClientID, strconv.FormatBool(b), req.Header.Get(HeaderClientID))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractClientID(t *testing.T) {
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
resp := mocks.NewResponse()
|
||||
mocks.SetResponseHeader(resp, HeaderClientID, uuid)
|
||||
|
||||
if ExtractClientID(resp) != uuid {
|
||||
t.Fatalf("azure: ExtractClientID failed to extract the %s -- expected %s, received %s",
|
||||
HeaderClientID, uuid, ExtractClientID(resp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRequestID(t *testing.T) {
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
resp := mocks.NewResponse()
|
||||
mocks.SetResponseHeader(resp, HeaderRequestID, uuid)
|
||||
|
||||
if ExtractRequestID(resp) != uuid {
|
||||
t.Fatalf("azure: ExtractRequestID failed to extract the %s -- expected %s, received %s",
|
||||
HeaderRequestID, uuid, ExtractRequestID(resp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAzureError_ReturnsTrueForAzureError(t *testing.T) {
|
||||
if !IsAzureError(&RequestError{}) {
|
||||
t.Fatalf("azure: IsAzureError failed to return true for an Azure Service error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAzureError_ReturnsFalseForNonAzureError(t *testing.T) {
|
||||
if IsAzureError(fmt.Errorf("An Error")) {
|
||||
t.Fatalf("azure: IsAzureError return true for an non-Azure Service error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewErrorWithError_UsesReponseStatusCode(t *testing.T) {
|
||||
e := NewErrorWithError(fmt.Errorf("Error"), "packageType", "method", mocks.NewResponseWithStatus("Forbidden", http.StatusForbidden), "message")
|
||||
if e.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("azure: NewErrorWithError failed to use the Status Code of the passed Response -- expected %v, received %v", http.StatusForbidden, e.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewErrorWithError_ReturnsUnwrappedError(t *testing.T) {
|
||||
e1 := RequestError{}
|
||||
e1.ServiceError = &ServiceError{Code: "42", Message: "A Message"}
|
||||
e1.StatusCode = 200
|
||||
e1.RequestID = "A RequestID"
|
||||
e2 := NewErrorWithError(&e1, "packageType", "method", nil, "message")
|
||||
|
||||
if !reflect.DeepEqual(e1, e2) {
|
||||
t.Fatalf("azure: NewErrorWithError wrapped an RequestError -- expected %T, received %T", e1, e2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewErrorWithError_WrapsAnError(t *testing.T) {
|
||||
e1 := fmt.Errorf("Inner Error")
|
||||
var e2 interface{} = NewErrorWithError(e1, "packageType", "method", nil, "message")
|
||||
|
||||
if _, ok := e2.(RequestError); !ok {
|
||||
t.Fatalf("azure: NewErrorWithError failed to wrap a standard error -- received %T", e2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithErrorUnlessStatusCode_NotAnAzureError(t *testing.T) {
|
||||
body := `<html>
|
||||
<head>
|
||||
<title>IIS Error page</title>
|
||||
</head>
|
||||
<body>Some non-JSON error page</body>
|
||||
</html>`
|
||||
r := mocks.NewResponseWithContent(body)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusBadRequest
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
ok, _ := err.(*RequestError)
|
||||
if ok != nil {
|
||||
t.Fatalf("azure: azure.RequestError returned from malformed response: %v", err)
|
||||
}
|
||||
|
||||
// the error body should still be there
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != body {
|
||||
t.Fatalf("response body is wrong. got=%q exptected=%q", string(b), body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithErrorUnlessStatusCode_FoundAzureErrorWithoutDetails(t *testing.T) {
|
||||
j := `{
|
||||
"error": {
|
||||
"code": "InternalError",
|
||||
"message": "Azure is having trouble right now."
|
||||
}
|
||||
}`
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
r := mocks.NewResponseWithContent(j)
|
||||
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("azure: returned nil error for proper error response")
|
||||
}
|
||||
azErr, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
t.Fatalf("azure: returned error is not azure.RequestError: %T", err)
|
||||
}
|
||||
|
||||
expected := "autorest/azure: Service returned an error. Status=500 Code=\"InternalError\" Message=\"Azure is having trouble right now.\""
|
||||
if !reflect.DeepEqual(expected, azErr.Error()) {
|
||||
t.Fatalf("azure: service error is not unmarshaled properly.\nexpected=%v\ngot=%v", expected, azErr.Error())
|
||||
}
|
||||
|
||||
if expected := http.StatusInternalServerError; azErr.StatusCode != expected {
|
||||
t.Fatalf("azure: got wrong StatusCode=%d Expected=%d", azErr.StatusCode, expected)
|
||||
}
|
||||
if expected := uuid; azErr.RequestID != expected {
|
||||
t.Fatalf("azure: wrong request ID in error. expected=%q; got=%q", expected, azErr.RequestID)
|
||||
}
|
||||
|
||||
_ = azErr.Error()
|
||||
|
||||
// the error body should still be there
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != j {
|
||||
t.Fatalf("response body is wrong. got=%q expected=%q", string(b), j)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestWithErrorUnlessStatusCode_FoundAzureErrorWithDetails(t *testing.T) {
|
||||
j := `{
|
||||
"error": {
|
||||
"code": "InternalError",
|
||||
"message": "Azure is having trouble right now.",
|
||||
"details": [{"code": "conflict1", "message":"error message1"},
|
||||
{"code": "conflict2", "message":"error message2"}]
|
||||
}
|
||||
}`
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
r := mocks.NewResponseWithContent(j)
|
||||
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("azure: returned nil error for proper error response")
|
||||
}
|
||||
azErr, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
t.Fatalf("azure: returned error is not azure.RequestError: %T", err)
|
||||
}
|
||||
|
||||
if expected := "InternalError"; azErr.ServiceError.Code != expected {
|
||||
t.Fatalf("azure: wrong error code. expected=%q; got=%q", expected, azErr.ServiceError.Code)
|
||||
}
|
||||
if azErr.ServiceError.Message == "" {
|
||||
t.Fatalf("azure: error message is not unmarshaled properly")
|
||||
}
|
||||
b, _ := json.Marshal(*azErr.ServiceError.Details)
|
||||
if string(b) != `[{"code":"conflict1","message":"error message1"},{"code":"conflict2","message":"error message2"}]` {
|
||||
t.Fatalf("azure: error details is not unmarshaled properly")
|
||||
}
|
||||
|
||||
if expected := http.StatusInternalServerError; azErr.StatusCode != expected {
|
||||
t.Fatalf("azure: got wrong StatusCode=%v Expected=%d", azErr.StatusCode, expected)
|
||||
}
|
||||
if expected := uuid; azErr.RequestID != expected {
|
||||
t.Fatalf("azure: wrong request ID in error. expected=%q; got=%q", expected, azErr.RequestID)
|
||||
}
|
||||
|
||||
_ = azErr.Error()
|
||||
|
||||
// the error body should still be there
|
||||
defer r.Body.Close()
|
||||
b, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != j {
|
||||
t.Fatalf("response body is wrong. got=%q expected=%q", string(b), j)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestWithErrorUnlessStatusCode_NoAzureError(t *testing.T) {
|
||||
j := `{
|
||||
"Status":"NotFound"
|
||||
}`
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
r := mocks.NewResponseWithContent(j)
|
||||
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
if err == nil {
|
||||
t.Fatalf("azure: returned nil error for proper error response")
|
||||
}
|
||||
azErr, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
t.Fatalf("azure: returned error is not azure.RequestError: %T", err)
|
||||
}
|
||||
|
||||
expected := &ServiceError{
|
||||
Code: "Unknown",
|
||||
Message: "Unknown service error",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, azErr.ServiceError) {
|
||||
t.Fatalf("azure: service error is not unmarshaled properly. expected=%q\ngot=%q", expected, azErr.ServiceError)
|
||||
}
|
||||
|
||||
if expected := http.StatusInternalServerError; azErr.StatusCode != expected {
|
||||
t.Fatalf("azure: got wrong StatusCode=%v Expected=%d", azErr.StatusCode, expected)
|
||||
}
|
||||
if expected := uuid; azErr.RequestID != expected {
|
||||
t.Fatalf("azure: wrong request ID in error. expected=%q; got=%q", expected, azErr.RequestID)
|
||||
}
|
||||
|
||||
_ = azErr.Error()
|
||||
|
||||
// the error body should still be there
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != j {
|
||||
t.Fatalf("response body is wrong. got=%q expected=%q", string(b), j)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestWithErrorUnlessStatusCode_UnwrappedError(t *testing.T) {
|
||||
j := `{
|
||||
"target": null,
|
||||
"code": "InternalError",
|
||||
"message": "Azure is having trouble right now.",
|
||||
"details": [{"code": "conflict1", "message":"error message1"},
|
||||
{"code": "conflict2", "message":"error message2"}],
|
||||
"innererror": []
|
||||
}`
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
r := mocks.NewResponseWithContent(j)
|
||||
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("azure: returned nil error for proper error response")
|
||||
}
|
||||
|
||||
azErr, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
t.Fatalf("returned error is not azure.RequestError: %T", err)
|
||||
}
|
||||
|
||||
if expected := http.StatusInternalServerError; azErr.StatusCode != expected {
|
||||
t.Logf("Incorrect StatusCode got: %v want: %d", azErr.StatusCode, expected)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if expected := "Azure is having trouble right now."; azErr.ServiceError.Message != expected {
|
||||
t.Logf("Incorrect Message\n\tgot: %q\n\twant: %q", azErr.Message, expected)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if expected := uuid; azErr.RequestID != expected {
|
||||
t.Logf("Incorrect request ID\n\tgot: %q\n\twant: %q", azErr.RequestID, expected)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
expectedServiceErrorDetails := `[{"code":"conflict1","message":"error message1"},{"code":"conflict2","message":"error message2"}]`
|
||||
if azErr.ServiceError == nil {
|
||||
t.Logf("`ServiceError` was nil when it shouldn't have been.")
|
||||
t.Fail()
|
||||
} else if azErr.ServiceError.Details == nil {
|
||||
t.Logf("`ServiceError.Details` was nil when it should have been %q", expectedServiceErrorDetails)
|
||||
t.Fail()
|
||||
} else if details, _ := json.Marshal(*azErr.ServiceError.Details); expectedServiceErrorDetails != string(details) {
|
||||
t.Logf("Error detaisl was not unmarshaled properly.\n\tgot: %q\n\twant: %q", string(details), expectedServiceErrorDetails)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
// the error body should still be there
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(b) != j {
|
||||
t.Fatalf("response body is wrong. got=%q expected=%q", string(b), j)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRequestErrorString_WithError(t *testing.T) {
|
||||
j := `{
|
||||
"error": {
|
||||
"code": "InternalError",
|
||||
"message": "Conflict",
|
||||
"details": [{"code": "conflict1", "message":"error message1"}]
|
||||
}
|
||||
}`
|
||||
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
|
||||
r := mocks.NewResponseWithContent(j)
|
||||
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
|
||||
r.Request = mocks.NewRequest()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
r.Status = http.StatusText(r.StatusCode)
|
||||
|
||||
err := autorest.Respond(r,
|
||||
WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByClosing())
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("azure: returned nil error for proper error response")
|
||||
}
|
||||
azErr, _ := err.(*RequestError)
|
||||
expected := "autorest/azure: Service returned an error. Status=500 Code=\"InternalError\" Message=\"Conflict\" Details=[{\"code\":\"conflict1\",\"message\":\"error message1\"}]"
|
||||
if expected != azErr.Error() {
|
||||
t.Fatalf("azure: send wrong RequestError.\nexpected=%v\ngot=%v", expected, azErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func withErrorPrepareDecorator(e *error) autorest.PrepareDecorator {
|
||||
return func(p autorest.Preparer) autorest.Preparer {
|
||||
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
*e = fmt.Errorf("azure: Faux Prepare Error")
|
||||
return r, *e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func withAsyncResponseDecorator(n int) autorest.SendDecorator {
|
||||
i := 0
|
||||
return func(s autorest.Sender) autorest.Sender {
|
||||
return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||
resp, err := s.Do(r)
|
||||
if err == nil {
|
||||
if i < n {
|
||||
resp.StatusCode = http.StatusCreated
|
||||
resp.Header = http.Header{}
|
||||
resp.Header.Add(http.CanonicalHeaderKey(headerAsyncOperation), mocks.TestURL)
|
||||
i++
|
||||
} else {
|
||||
resp.StatusCode = http.StatusOK
|
||||
resp.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockAuthorizer struct{}
|
||||
|
||||
func (ma mockAuthorizer) WithAuthorization() autorest.PrepareDecorator {
|
||||
return autorest.WithHeader(headerAuthorization, mocks.TestAuthorizationHeader)
|
||||
}
|
||||
|
||||
type mockFailingAuthorizer struct{}
|
||||
|
||||
func (mfa mockFailingAuthorizer) WithAuthorization() autorest.PrepareDecorator {
|
||||
return func(p autorest.Preparer) autorest.Preparer {
|
||||
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
return r, fmt.Errorf("ERROR: mockFailingAuthorizer returned expected error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockInspector struct {
|
||||
wasInvoked bool
|
||||
}
|
||||
|
||||
func (mi *mockInspector) WithInspection() autorest.PrepareDecorator {
|
||||
return func(p autorest.Preparer) autorest.Preparer {
|
||||
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
mi.wasInvoked = true
|
||||
return p.Prepare(r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (mi *mockInspector) ByInspecting() autorest.RespondDecorator {
|
||||
return func(r autorest.Responder) autorest.Responder {
|
||||
return autorest.ResponderFunc(func(resp *http.Response) error {
|
||||
mi.wasInvoked = true
|
||||
return r.Respond(resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
65
vendor/github.com/Azure/go-autorest/autorest/azure/cli/profile.go
generated
vendored
Normal file
65
vendor/github.com/Azure/go-autorest/autorest/azure/cli/profile.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package cli
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dimchansky/utfbom"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Profile represents a Profile from the Azure CLI
|
||||
type Profile struct {
|
||||
InstallationID string `json:"installationId"`
|
||||
Subscriptions []Subscription `json:"subscriptions"`
|
||||
}
|
||||
|
||||
// Subscription represents a Subscription from the Azure CLI
|
||||
type Subscription struct {
|
||||
EnvironmentName string `json:"environmentName"`
|
||||
ID string `json:"id"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
TenantID string `json:"tenantId"`
|
||||
}
|
||||
|
||||
// ProfilePath returns the path where the Azure Profile is stored from the Azure CLI
|
||||
func ProfilePath() (string, error) {
|
||||
return homedir.Expand("~/.azure/azureProfile.json")
|
||||
}
|
||||
|
||||
// LoadProfile restores a Profile object from a file located at 'path'.
|
||||
func LoadProfile(path string) (result Profile, err error) {
|
||||
var contents []byte
|
||||
contents, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
|
||||
return
|
||||
}
|
||||
reader := utfbom.SkipOnly(bytes.NewReader(contents))
|
||||
|
||||
dec := json.NewDecoder(reader)
|
||||
if err = dec.Decode(&result); err != nil {
|
||||
err = fmt.Errorf("failed to decode contents of file (%s) into a Profile representation: %v", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
114
vendor/github.com/Azure/go-autorest/autorest/azure/cli/token.go
generated
vendored
Normal file
114
vendor/github.com/Azure/go-autorest/autorest/azure/cli/token.go
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
package cli
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/date"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Token represents an AccessToken from the Azure CLI
|
||||
type Token struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
Authority string `json:"_authority"`
|
||||
ClientID string `json:"_clientId"`
|
||||
ExpiresOn string `json:"expiresOn"`
|
||||
IdentityProvider string `json:"identityProvider"`
|
||||
IsMRRT bool `json:"isMRRT"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
Resource string `json:"resource"`
|
||||
TokenType string `json:"tokenType"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
|
||||
// ToADALToken converts an Azure CLI `Token`` to an `adal.Token``
|
||||
func (t Token) ToADALToken() (converted adal.Token, err error) {
|
||||
tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
|
||||
return
|
||||
}
|
||||
|
||||
difference := tokenExpirationDate.Sub(date.UnixEpoch())
|
||||
|
||||
converted = adal.Token{
|
||||
AccessToken: t.AccessToken,
|
||||
Type: t.TokenType,
|
||||
ExpiresIn: "3600",
|
||||
ExpiresOn: strconv.Itoa(int(difference.Seconds())),
|
||||
RefreshToken: t.RefreshToken,
|
||||
Resource: t.Resource,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AccessTokensPath returns the path where access tokens are stored from the Azure CLI
|
||||
// TODO(#199): add unit test.
|
||||
func AccessTokensPath() (string, error) {
|
||||
// Azure-CLI allows user to customize the path of access tokens thorugh environment variable.
|
||||
var accessTokenPath = os.Getenv("AZURE_ACCESS_TOKEN_FILE")
|
||||
var err error
|
||||
|
||||
// Fallback logic to default path on non-cloud-shell environment.
|
||||
// TODO(#200): remove the dependency on hard-coding path.
|
||||
if accessTokenPath == "" {
|
||||
accessTokenPath, err = homedir.Expand("~/.azure/accessTokens.json")
|
||||
}
|
||||
|
||||
return accessTokenPath, err
|
||||
}
|
||||
|
||||
// ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object
|
||||
func ParseExpirationDate(input string) (*time.Time, error) {
|
||||
// CloudShell (and potentially the Azure CLI in future)
|
||||
expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
|
||||
if cloudShellErr != nil {
|
||||
// Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone)
|
||||
const cliFormat = "2006-01-02 15:04:05.999999"
|
||||
expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
|
||||
if cliErr == nil {
|
||||
return &expirationDate, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
|
||||
}
|
||||
|
||||
return &expirationDate, nil
|
||||
}
|
||||
|
||||
// LoadTokens restores a set of Token objects from a file located at 'path'.
|
||||
func LoadTokens(path string) ([]Token, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var tokens []Token
|
||||
|
||||
dec := json.NewDecoder(file)
|
||||
if err = dec.Decode(&tokens); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err)
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
176
vendor/github.com/Azure/go-autorest/autorest/azure/environments.go
generated
vendored
Normal file
176
vendor/github.com/Azure/go-autorest/autorest/azure/environments.go
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
package azure
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
|
||||
// to be used while populating the Azure Environment.
|
||||
const EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
|
||||
|
||||
var environments = map[string]Environment{
|
||||
"AZURECHINACLOUD": ChinaCloud,
|
||||
"AZUREGERMANCLOUD": GermanCloud,
|
||||
"AZUREPUBLICCLOUD": PublicCloud,
|
||||
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud,
|
||||
}
|
||||
|
||||
// Environment represents a set of endpoints for each of Azure's Clouds.
|
||||
type Environment struct {
|
||||
Name string `json:"name"`
|
||||
ManagementPortalURL string `json:"managementPortalURL"`
|
||||
PublishSettingsURL string `json:"publishSettingsURL"`
|
||||
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
||||
GalleryEndpoint string `json:"galleryEndpoint"`
|
||||
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
|
||||
GraphEndpoint string `json:"graphEndpoint"`
|
||||
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
|
||||
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
|
||||
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
|
||||
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
|
||||
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
|
||||
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
|
||||
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
|
||||
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
|
||||
}
|
||||
|
||||
var (
|
||||
// PublicCloud is the default public Azure cloud environment
|
||||
PublicCloud = Environment{
|
||||
Name: "AzurePublicCloud",
|
||||
ManagementPortalURL: "https://manage.windowsazure.com/",
|
||||
PublishSettingsURL: "https://manage.windowsazure.com/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.windows.net/",
|
||||
ResourceManagerEndpoint: "https://management.azure.com/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
||||
GalleryEndpoint: "https://gallery.azure.com/",
|
||||
KeyVaultEndpoint: "https://vault.azure.net/",
|
||||
GraphEndpoint: "https://graph.windows.net/",
|
||||
StorageEndpointSuffix: "core.windows.net",
|
||||
SQLDatabaseDNSSuffix: "database.windows.net",
|
||||
TrafficManagerDNSSuffix: "trafficmanager.net",
|
||||
KeyVaultDNSSuffix: "vault.azure.net",
|
||||
ServiceBusEndpointSuffix: "servicebus.azure.com",
|
||||
ServiceManagementVMDNSSuffix: "cloudapp.net",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// USGovernmentCloud is the cloud environment for the US Government
|
||||
USGovernmentCloud = Environment{
|
||||
Name: "AzureUSGovernmentCloud",
|
||||
ManagementPortalURL: "https://manage.windowsazure.us/",
|
||||
PublishSettingsURL: "https://manage.windowsazure.us/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.usgovcloudapi.net/",
|
||||
ResourceManagerEndpoint: "https://management.usgovcloudapi.net/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
||||
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
|
||||
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
|
||||
GraphEndpoint: "https://graph.usgovcloudapi.net/",
|
||||
StorageEndpointSuffix: "core.usgovcloudapi.net",
|
||||
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
|
||||
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
|
||||
KeyVaultDNSSuffix: "vault.usgovcloudapi.net",
|
||||
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
|
||||
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// ChinaCloud is the cloud environment operated in China
|
||||
ChinaCloud = Environment{
|
||||
Name: "AzureChinaCloud",
|
||||
ManagementPortalURL: "https://manage.chinacloudapi.com/",
|
||||
PublishSettingsURL: "https://manage.chinacloudapi.com/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.chinacloudapi.cn/",
|
||||
ResourceManagerEndpoint: "https://management.chinacloudapi.cn/",
|
||||
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn/",
|
||||
GalleryEndpoint: "https://gallery.chinacloudapi.cn/",
|
||||
KeyVaultEndpoint: "https://vault.azure.cn/",
|
||||
GraphEndpoint: "https://graph.chinacloudapi.cn/",
|
||||
StorageEndpointSuffix: "core.chinacloudapi.cn",
|
||||
SQLDatabaseDNSSuffix: "database.chinacloudapi.cn",
|
||||
TrafficManagerDNSSuffix: "trafficmanager.cn",
|
||||
KeyVaultDNSSuffix: "vault.azure.cn",
|
||||
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.net",
|
||||
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
|
||||
// GermanCloud is the cloud environment operated in Germany
|
||||
GermanCloud = Environment{
|
||||
Name: "AzureGermanCloud",
|
||||
ManagementPortalURL: "http://portal.microsoftazure.de/",
|
||||
PublishSettingsURL: "https://manage.microsoftazure.de/publishsettings/index",
|
||||
ServiceManagementEndpoint: "https://management.core.cloudapi.de/",
|
||||
ResourceManagerEndpoint: "https://management.microsoftazure.de/",
|
||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.de/",
|
||||
GalleryEndpoint: "https://gallery.cloudapi.de/",
|
||||
KeyVaultEndpoint: "https://vault.microsoftazure.de/",
|
||||
GraphEndpoint: "https://graph.cloudapi.de/",
|
||||
StorageEndpointSuffix: "core.cloudapi.de",
|
||||
SQLDatabaseDNSSuffix: "database.cloudapi.de",
|
||||
TrafficManagerDNSSuffix: "azuretrafficmanager.de",
|
||||
KeyVaultDNSSuffix: "vault.microsoftazure.de",
|
||||
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
|
||||
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
|
||||
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
|
||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||
}
|
||||
)
|
||||
|
||||
// EnvironmentFromName returns an Environment based on the common name specified.
|
||||
func EnvironmentFromName(name string) (Environment, error) {
|
||||
// IMPORTANT
|
||||
// As per @radhikagupta5:
|
||||
// This is technical debt, fundamentally here because Kubernetes is not currently accepting
|
||||
// contributions to the providers. Once that is an option, the provider should be updated to
|
||||
// directly call `EnvironmentFromFile`. Until then, we rely on dispatching Azure Stack environment creation
|
||||
// from this method based on the name that is provided to us.
|
||||
if strings.EqualFold(name, "AZURESTACKCLOUD") {
|
||||
return EnvironmentFromFile(os.Getenv(EnvironmentFilepathName))
|
||||
}
|
||||
|
||||
name = strings.ToUpper(name)
|
||||
env, ok := environments[name]
|
||||
if !ok {
|
||||
return env, fmt.Errorf("autorest/azure: There is no cloud environment matching the name %q", name)
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// EnvironmentFromFile loads an Environment from a configuration file available on disk.
|
||||
// This function is particularly useful in the Hybrid Cloud model, where one must define their own
|
||||
// endpoints.
|
||||
func EnvironmentFromFile(location string) (unmarshaled Environment, err error) {
|
||||
fileContents, err := ioutil.ReadFile(location)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(fileContents, &unmarshaled)
|
||||
|
||||
return
|
||||
}
|
||||
284
vendor/github.com/Azure/go-autorest/autorest/azure/environments_test.go
generated
vendored
Normal file
284
vendor/github.com/Azure/go-autorest/autorest/azure/environments_test.go
generated
vendored
Normal file
@@ -0,0 +1,284 @@
|
||||
// test
|
||||
package azure
|
||||
|
||||
// Copyright 2017 Microsoft Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This correlates to the expected contents of ./testdata/test_environment_1.json
|
||||
var testEnvironment1 = Environment{
|
||||
Name: "--unit-test--",
|
||||
ManagementPortalURL: "--management-portal-url",
|
||||
PublishSettingsURL: "--publish-settings-url--",
|
||||
ServiceManagementEndpoint: "--service-management-endpoint--",
|
||||
ResourceManagerEndpoint: "--resource-management-endpoint--",
|
||||
ActiveDirectoryEndpoint: "--active-directory-endpoint--",
|
||||
GalleryEndpoint: "--gallery-endpoint--",
|
||||
KeyVaultEndpoint: "--key-vault--endpoint--",
|
||||
GraphEndpoint: "--graph-endpoint--",
|
||||
StorageEndpointSuffix: "--storage-endpoint-suffix--",
|
||||
SQLDatabaseDNSSuffix: "--sql-database-dns-suffix--",
|
||||
TrafficManagerDNSSuffix: "--traffic-manager-dns-suffix--",
|
||||
KeyVaultDNSSuffix: "--key-vault-dns-suffix--",
|
||||
ServiceBusEndpointSuffix: "--service-bus-endpoint-suffix--",
|
||||
ServiceManagementVMDNSSuffix: "--asm-vm-dns-suffix--",
|
||||
ResourceManagerVMDNSSuffix: "--arm-vm-dns-suffix--",
|
||||
ContainerRegistryDNSSuffix: "--container-registry-dns-suffix--",
|
||||
}
|
||||
|
||||
func TestEnvironment_EnvironmentFromFile(t *testing.T) {
|
||||
got, err := EnvironmentFromFile(filepath.Join("testdata", "test_environment_1.json"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if got != testEnvironment1 {
|
||||
t.Logf("got: %v want: %v", got, testEnvironment1)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_EnvironmentFromName_Stack(t *testing.T) {
|
||||
_, currentFile, _, _ := runtime.Caller(0)
|
||||
prevEnvFilepathValue := os.Getenv(EnvironmentFilepathName)
|
||||
os.Setenv(EnvironmentFilepathName, filepath.Join(path.Dir(currentFile), "testdata", "test_environment_1.json"))
|
||||
defer os.Setenv(EnvironmentFilepathName, prevEnvFilepathValue)
|
||||
|
||||
got, err := EnvironmentFromName("AZURESTACKCLOUD")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if got != testEnvironment1 {
|
||||
t.Logf("got: %v want: %v", got, testEnvironment1)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentFromName(t *testing.T) {
|
||||
name := "azurechinacloud"
|
||||
if env, _ := EnvironmentFromName(name); env != ChinaCloud {
|
||||
t.Errorf("Expected to get ChinaCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "AzureChinaCloud"
|
||||
if env, _ := EnvironmentFromName(name); env != ChinaCloud {
|
||||
t.Errorf("Expected to get ChinaCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "azuregermancloud"
|
||||
if env, _ := EnvironmentFromName(name); env != GermanCloud {
|
||||
t.Errorf("Expected to get GermanCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "AzureGermanCloud"
|
||||
if env, _ := EnvironmentFromName(name); env != GermanCloud {
|
||||
t.Errorf("Expected to get GermanCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "azurepubliccloud"
|
||||
if env, _ := EnvironmentFromName(name); env != PublicCloud {
|
||||
t.Errorf("Expected to get PublicCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "AzurePublicCloud"
|
||||
if env, _ := EnvironmentFromName(name); env != PublicCloud {
|
||||
t.Errorf("Expected to get PublicCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "azureusgovernmentcloud"
|
||||
if env, _ := EnvironmentFromName(name); env != USGovernmentCloud {
|
||||
t.Errorf("Expected to get USGovernmentCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "AzureUSGovernmentCloud"
|
||||
if env, _ := EnvironmentFromName(name); env != USGovernmentCloud {
|
||||
t.Errorf("Expected to get USGovernmentCloud for %q", name)
|
||||
}
|
||||
|
||||
name = "thisisnotarealcloudenv"
|
||||
if _, err := EnvironmentFromName(name); err == nil {
|
||||
t.Errorf("Expected to get an error for %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeserializeEnvironment(t *testing.T) {
|
||||
env := `{
|
||||
"name": "--name--",
|
||||
"ActiveDirectoryEndpoint": "--active-directory-endpoint--",
|
||||
"galleryEndpoint": "--gallery-endpoint--",
|
||||
"graphEndpoint": "--graph-endpoint--",
|
||||
"keyVaultDNSSuffix": "--key-vault-dns-suffix--",
|
||||
"keyVaultEndpoint": "--key-vault-endpoint--",
|
||||
"managementPortalURL": "--management-portal-url--",
|
||||
"publishSettingsURL": "--publish-settings-url--",
|
||||
"resourceManagerEndpoint": "--resource-manager-endpoint--",
|
||||
"serviceBusEndpointSuffix": "--service-bus-endpoint-suffix--",
|
||||
"serviceManagementEndpoint": "--service-management-endpoint--",
|
||||
"sqlDatabaseDNSSuffix": "--sql-database-dns-suffix--",
|
||||
"storageEndpointSuffix": "--storage-endpoint-suffix--",
|
||||
"trafficManagerDNSSuffix": "--traffic-manager-dns-suffix--",
|
||||
"serviceManagementVMDNSSuffix": "--asm-vm-dns-suffix--",
|
||||
"resourceManagerVMDNSSuffix": "--arm-vm-dns-suffix--",
|
||||
"containerRegistryDNSSuffix": "--container-registry-dns-suffix--"
|
||||
}`
|
||||
|
||||
testSubject := Environment{}
|
||||
err := json.Unmarshal([]byte(env), &testSubject)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal: %s", err)
|
||||
}
|
||||
|
||||
if "--name--" != testSubject.Name {
|
||||
t.Errorf("Expected Name to be \"--name--\", but got %q", testSubject.Name)
|
||||
}
|
||||
if "--management-portal-url--" != testSubject.ManagementPortalURL {
|
||||
t.Errorf("Expected ManagementPortalURL to be \"--management-portal-url--\", but got %q", testSubject.ManagementPortalURL)
|
||||
}
|
||||
if "--publish-settings-url--" != testSubject.PublishSettingsURL {
|
||||
t.Errorf("Expected PublishSettingsURL to be \"--publish-settings-url--\", but got %q", testSubject.PublishSettingsURL)
|
||||
}
|
||||
if "--service-management-endpoint--" != testSubject.ServiceManagementEndpoint {
|
||||
t.Errorf("Expected ServiceManagementEndpoint to be \"--service-management-endpoint--\", but got %q", testSubject.ServiceManagementEndpoint)
|
||||
}
|
||||
if "--resource-manager-endpoint--" != testSubject.ResourceManagerEndpoint {
|
||||
t.Errorf("Expected ResourceManagerEndpoint to be \"--resource-manager-endpoint--\", but got %q", testSubject.ResourceManagerEndpoint)
|
||||
}
|
||||
if "--active-directory-endpoint--" != testSubject.ActiveDirectoryEndpoint {
|
||||
t.Errorf("Expected ActiveDirectoryEndpoint to be \"--active-directory-endpoint--\", but got %q", testSubject.ActiveDirectoryEndpoint)
|
||||
}
|
||||
if "--gallery-endpoint--" != testSubject.GalleryEndpoint {
|
||||
t.Errorf("Expected GalleryEndpoint to be \"--gallery-endpoint--\", but got %q", testSubject.GalleryEndpoint)
|
||||
}
|
||||
if "--key-vault-endpoint--" != testSubject.KeyVaultEndpoint {
|
||||
t.Errorf("Expected KeyVaultEndpoint to be \"--key-vault-endpoint--\", but got %q", testSubject.KeyVaultEndpoint)
|
||||
}
|
||||
if "--graph-endpoint--" != testSubject.GraphEndpoint {
|
||||
t.Errorf("Expected GraphEndpoint to be \"--graph-endpoint--\", but got %q", testSubject.GraphEndpoint)
|
||||
}
|
||||
if "--storage-endpoint-suffix--" != testSubject.StorageEndpointSuffix {
|
||||
t.Errorf("Expected StorageEndpointSuffix to be \"--storage-endpoint-suffix--\", but got %q", testSubject.StorageEndpointSuffix)
|
||||
}
|
||||
if "--sql-database-dns-suffix--" != testSubject.SQLDatabaseDNSSuffix {
|
||||
t.Errorf("Expected sql-database-dns-suffix to be \"--sql-database-dns-suffix--\", but got %q", testSubject.SQLDatabaseDNSSuffix)
|
||||
}
|
||||
if "--key-vault-dns-suffix--" != testSubject.KeyVaultDNSSuffix {
|
||||
t.Errorf("Expected StorageEndpointSuffix to be \"--key-vault-dns-suffix--\", but got %q", testSubject.KeyVaultDNSSuffix)
|
||||
}
|
||||
if "--service-bus-endpoint-suffix--" != testSubject.ServiceBusEndpointSuffix {
|
||||
t.Errorf("Expected StorageEndpointSuffix to be \"--service-bus-endpoint-suffix--\", but got %q", testSubject.ServiceBusEndpointSuffix)
|
||||
}
|
||||
if "--asm-vm-dns-suffix--" != testSubject.ServiceManagementVMDNSSuffix {
|
||||
t.Errorf("Expected ServiceManagementVMDNSSuffix to be \"--asm-vm-dns-suffix--\", but got %q", testSubject.ServiceManagementVMDNSSuffix)
|
||||
}
|
||||
if "--arm-vm-dns-suffix--" != testSubject.ResourceManagerVMDNSSuffix {
|
||||
t.Errorf("Expected ResourceManagerVMDNSSuffix to be \"--arm-vm-dns-suffix--\", but got %q", testSubject.ResourceManagerVMDNSSuffix)
|
||||
}
|
||||
if "--container-registry-dns-suffix--" != testSubject.ContainerRegistryDNSSuffix {
|
||||
t.Errorf("Expected ContainerRegistryDNSSuffix to be \"--container-registry-dns-suffix--\", but got %q", testSubject.ContainerRegistryDNSSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripSerialization(t *testing.T) {
|
||||
env := Environment{
|
||||
Name: "--unit-test--",
|
||||
ManagementPortalURL: "--management-portal-url",
|
||||
PublishSettingsURL: "--publish-settings-url--",
|
||||
ServiceManagementEndpoint: "--service-management-endpoint--",
|
||||
ResourceManagerEndpoint: "--resource-management-endpoint--",
|
||||
ActiveDirectoryEndpoint: "--active-directory-endpoint--",
|
||||
GalleryEndpoint: "--gallery-endpoint--",
|
||||
KeyVaultEndpoint: "--key-vault--endpoint--",
|
||||
GraphEndpoint: "--graph-endpoint--",
|
||||
StorageEndpointSuffix: "--storage-endpoint-suffix--",
|
||||
SQLDatabaseDNSSuffix: "--sql-database-dns-suffix--",
|
||||
TrafficManagerDNSSuffix: "--traffic-manager-dns-suffix--",
|
||||
KeyVaultDNSSuffix: "--key-vault-dns-suffix--",
|
||||
ServiceBusEndpointSuffix: "--service-bus-endpoint-suffix--",
|
||||
ServiceManagementVMDNSSuffix: "--asm-vm-dns-suffix--",
|
||||
ResourceManagerVMDNSSuffix: "--arm-vm-dns-suffix--",
|
||||
ContainerRegistryDNSSuffix: "--container-registry-dns-suffix--",
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %s", err)
|
||||
}
|
||||
|
||||
testSubject := Environment{}
|
||||
err = json.Unmarshal(bytes, &testSubject)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal: %s", err)
|
||||
}
|
||||
|
||||
if env.Name != testSubject.Name {
|
||||
t.Errorf("Expected Name to be %q, but got %q", env.Name, testSubject.Name)
|
||||
}
|
||||
if env.ManagementPortalURL != testSubject.ManagementPortalURL {
|
||||
t.Errorf("Expected ManagementPortalURL to be %q, but got %q", env.ManagementPortalURL, testSubject.ManagementPortalURL)
|
||||
}
|
||||
if env.PublishSettingsURL != testSubject.PublishSettingsURL {
|
||||
t.Errorf("Expected PublishSettingsURL to be %q, but got %q", env.PublishSettingsURL, testSubject.PublishSettingsURL)
|
||||
}
|
||||
if env.ServiceManagementEndpoint != testSubject.ServiceManagementEndpoint {
|
||||
t.Errorf("Expected ServiceManagementEndpoint to be %q, but got %q", env.ServiceManagementEndpoint, testSubject.ServiceManagementEndpoint)
|
||||
}
|
||||
if env.ResourceManagerEndpoint != testSubject.ResourceManagerEndpoint {
|
||||
t.Errorf("Expected ResourceManagerEndpoint to be %q, but got %q", env.ResourceManagerEndpoint, testSubject.ResourceManagerEndpoint)
|
||||
}
|
||||
if env.ActiveDirectoryEndpoint != testSubject.ActiveDirectoryEndpoint {
|
||||
t.Errorf("Expected ActiveDirectoryEndpoint to be %q, but got %q", env.ActiveDirectoryEndpoint, testSubject.ActiveDirectoryEndpoint)
|
||||
}
|
||||
if env.GalleryEndpoint != testSubject.GalleryEndpoint {
|
||||
t.Errorf("Expected GalleryEndpoint to be %q, but got %q", env.GalleryEndpoint, testSubject.GalleryEndpoint)
|
||||
}
|
||||
if env.KeyVaultEndpoint != testSubject.KeyVaultEndpoint {
|
||||
t.Errorf("Expected KeyVaultEndpoint to be %q, but got %q", env.KeyVaultEndpoint, testSubject.KeyVaultEndpoint)
|
||||
}
|
||||
if env.GraphEndpoint != testSubject.GraphEndpoint {
|
||||
t.Errorf("Expected GraphEndpoint to be %q, but got %q", env.GraphEndpoint, testSubject.GraphEndpoint)
|
||||
}
|
||||
if env.StorageEndpointSuffix != testSubject.StorageEndpointSuffix {
|
||||
t.Errorf("Expected StorageEndpointSuffix to be %q, but got %q", env.StorageEndpointSuffix, testSubject.StorageEndpointSuffix)
|
||||
}
|
||||
if env.SQLDatabaseDNSSuffix != testSubject.SQLDatabaseDNSSuffix {
|
||||
t.Errorf("Expected SQLDatabaseDNSSuffix to be %q, but got %q", env.SQLDatabaseDNSSuffix, testSubject.SQLDatabaseDNSSuffix)
|
||||
}
|
||||
if env.TrafficManagerDNSSuffix != testSubject.TrafficManagerDNSSuffix {
|
||||
t.Errorf("Expected TrafficManagerDNSSuffix to be %q, but got %q", env.TrafficManagerDNSSuffix, testSubject.TrafficManagerDNSSuffix)
|
||||
}
|
||||
if env.KeyVaultDNSSuffix != testSubject.KeyVaultDNSSuffix {
|
||||
t.Errorf("Expected KeyVaultDNSSuffix to be %q, but got %q", env.KeyVaultDNSSuffix, testSubject.KeyVaultDNSSuffix)
|
||||
}
|
||||
if env.ServiceBusEndpointSuffix != testSubject.ServiceBusEndpointSuffix {
|
||||
t.Errorf("Expected ServiceBusEndpointSuffix to be %q, but got %q", env.ServiceBusEndpointSuffix, testSubject.ServiceBusEndpointSuffix)
|
||||
}
|
||||
if env.ServiceManagementVMDNSSuffix != testSubject.ServiceManagementVMDNSSuffix {
|
||||
t.Errorf("Expected ServiceManagementVMDNSSuffix to be %q, but got %q", env.ServiceManagementVMDNSSuffix, testSubject.ServiceManagementVMDNSSuffix)
|
||||
}
|
||||
if env.ResourceManagerVMDNSSuffix != testSubject.ResourceManagerVMDNSSuffix {
|
||||
t.Errorf("Expected ResourceManagerVMDNSSuffix to be %q, but got %q", env.ResourceManagerVMDNSSuffix, testSubject.ResourceManagerVMDNSSuffix)
|
||||
}
|
||||
if env.ContainerRegistryDNSSuffix != testSubject.ContainerRegistryDNSSuffix {
|
||||
t.Errorf("Expected ContainerRegistryDNSSuffix to be %q, but got %q", env.ContainerRegistryDNSSuffix, testSubject.ContainerRegistryDNSSuffix)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user