2
0
镜像自地址 https://github.com/docker/compose.git 已同步 2024-05-30 13:43:22 +08:00

move compose-cli code into docker/compose/v2

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
这个提交包含在:
Nicolas De Loof 2021-08-31 18:53:24 +02:00
父节点 c944c970ab
当前提交 1ae9b3cb5d
找不到此签名对应的密钥
GPG 密钥 ID: 9858809D6F8F6E7E
共有 401 个文件被更改,包括 199 次插入41265 次删除

查看文件

@ -1,4 +1,3 @@
.git/
bin/
dist/
tests/node-client/node_modules/

查看文件

@ -49,29 +49,16 @@ Briefly describe the problem you are having in a few paragraphs.
**Additional information you deem important (e.g. issue happens only occasionally):**
**Output of `docker-compose --version`:**
**Output of `docker compose version`:**
```
(paste your output here)
```
**Output of `docker version`:**
```
(paste your output here)
```
**Output of `docker context show`:**
You can also run `docker context inspect context-name` to give us more details but don't forget to remove sensitive content.
```
(paste your output here)
```
**Output of `docker info`:**
```
(paste your output here)
```
**Additional environment details (AWS ECS, Azure ACI, local, etc.):**
**Additional environment details:**

查看文件

@ -3,12 +3,4 @@
**Related issue**
<!-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
<!-- optional tests
You can add a / mention to run tests executed by default only on main branch :
* `test-kube` to run Kube E2E tests
* `test-aci` to run ACI E2E tests
* `test-ecs` to run ECS E2E tests
* `test-windows` to run tests & E2E tests on windows
-->
**(not mandatory) A picture of a cute animal, if possible in relation with what you did**

27
.github/labeler.yml vendored
查看文件

@ -1,27 +0,0 @@
aci:
- aci/**/*
ecs:
- ecs/**/*
local:
- local/**/*
kube:
- kube/**/*
cli:
- cli/**/*
metrics:
- cli/metrics/**/*
api:
- api/**/*
- cli/server/protos/**/*
ci:
- .github/**/*
documentation:
- docs/**/*

查看文件

@ -1,58 +0,0 @@
name: ACI integration tests
on:
push:
branches:
- main
pull_request:
jobs:
check-optional-tests:
name: Check if needs to run ACI tests
runs-on: ubuntu-latest
outputs:
trigger-aci: ${{steps.runacitest.outputs.triggered}}
steps:
- uses: khan/pull-request-comment-trigger@master
name: Check if test ACI
if: github.event_name == 'pull_request'
id: runacitest
with:
trigger: '/test-aci'
aci-tests:
name: ACI e2e tests
runs-on: ubuntu-latest
env:
GO111MODULE: "on"
needs: check-optional-tests
if: github.ref == 'refs/heads/main' || needs.check-optional-tests.outputs.trigger-aci == 'true'
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go
- name: Setup docker CLI
run: |
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Build for ACI e2e tests
run: make -f builder.Makefile cli
- name: ACI e2e Test
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
run: make e2e-aci

查看文件

@ -3,7 +3,7 @@ name: Continuous integration
on:
push:
branches:
- main
- v2
pull_request:
jobs:
@ -25,12 +25,9 @@ jobs:
- name: Validate go-mod is up-to-date and license headers
run: make validate
- name: Validate imports
run: make import-restrictions
- name: Run golangci-lint
env:
BUILD_TAGS: kube,e2e
BUILD_TAGS: e2e
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin/ v1.39.0
make -f builder.Makefile lint
@ -60,7 +57,7 @@ jobs:
# Ensure we don't discover cross platform build issues at release time.
# Time used to build linux here is gained back in the build for local E2E step
- name: Build packages
run: make -f builder.Makefile cross cross-compose-plugin
run: make -f builder.Makefile cross
build:
name: Build
@ -99,7 +96,7 @@ jobs:
- name: Build for local E2E
env:
BUILD_TAGS: e2e
run: make -f builder.Makefile cli compose-plugin
run: make -f builder.Makefile compose-plugin
- name: E2E Test
run: make e2e-compose

查看文件

@ -1,50 +0,0 @@
name: Releaser
on:
workflow_dispatch:
inputs:
tag:
description: 'Release Tag'
required: true
dry-run:
description: 'Dry run'
required: false
default: 'true'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go
- name: Setup docker CLI
run: |
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build
run: make -f builder.Makefile cross
- name: License
run: cp packaging/* bin/
- uses: ncipollo/release-action@v1
if: ${{ github.event.inputs.dry-run != 'true' }}
with:
artifacts: "bin/*"
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.event.inputs.tag }}

查看文件

@ -1,62 +0,0 @@
name: ECS integration tests
on:
push:
branches:
- main
pull_request:
jobs:
check-optional-tests:
name: Check if needs to run ECS tests
runs-on: ubuntu-latest
outputs:
trigger-ecs: ${{steps.runecstest.outputs.triggered}}
steps:
- uses: khan/pull-request-comment-trigger@master
name: Check if test ECS
if: github.event_name == 'pull_request'
id: runecstest
with:
trigger: '/test-ecs'
ecs-tests:
name: ECS e2e tests
runs-on: ubuntu-latest
env:
GO111MODULE: "on"
needs: check-optional-tests
if: github.ref == 'refs/heads/main' || needs.check-optional-tests.outputs.trigger-ecs == 'true'
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go
- name: Setup docker CLI
run: |
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Build for ECS e2e tests
run: make -f builder.Makefile cli
- name: create aws config folder
run: mkdir -p ~/.aws
- name: ECS e2e Test
env:
AWS_DEFAULT_REGION: us-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
run: make e2e-ecs

查看文件

@ -1,63 +0,0 @@
name: Kube integration tests
on:
push:
branches:
- main
pull_request:
jobs:
check-optional-tests:
name: Check if needs to run Kube tests
runs-on: ubuntu-latest
outputs:
trigger-kube: ${{steps.runkubetest.outputs.triggered}}
steps:
- uses: khan/pull-request-comment-trigger@master
name: Check if test Kube
if: github.event_name == 'pull_request'
id: runkubetest
with:
trigger: '/test-kube'
kube-tests:
name: Kube e2e tests
runs-on: ubuntu-latest
env:
GO111MODULE: "on"
needs: check-optional-tests
if: github.ref == 'refs/heads/main' || needs.check-optional-tests.outputs.trigger-kube == 'true'
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go
- name: Setup docker CLI
run: |
curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.3.tgz | tar xz
sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
- name: Setup Kube tools
run: |
sudo apt-get install jq && jq --version
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.10.0/kind-linux-amd64 && chmod +x ./kind && sudo mv ./kind /usr/bin/ && kind version
curl -LO "https://dl.k8s.io/release/v1.20.2/bin/linux/amd64/kubectl" && sudo mv kubectl /usr/bin/ && kubectl version --client
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Build for Kube e2e tests
env:
BUILD_TAGS: kube
run: make -f builder.Makefile cli
- name: Kube e2e Test
run: make e2e-kube

查看文件

@ -1,11 +0,0 @@
name: 'PR labeler'
on:
- pull_request_target
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v2.1.1
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'

查看文件

@ -1,17 +0,0 @@
name: Remove pending label on answer
on:
issue_comment:
types: [ created ]
jobs:
pending:
if: ${{ !github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'pending') }}
name: Remove pending label
runs-on: ubuntu-latest
steps:
- name: removelabel
uses: siegerts/pending-response@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
pending-response-label: pending

查看文件

@ -1,72 +0,0 @@
name: Windows CI
on:
push:
branches:
- main
pull_request:
jobs:
check-optional-tests:
name: Check if needs to run Windows build and tests
runs-on: ubuntu-latest
outputs:
trigger-windows: ${{steps.runwindowstest.outputs.triggered}}
steps:
- uses: khan/pull-request-comment-trigger@master
name: Check if test Windows
if: github.event_name == 'pull_request'
id: runwindowstest
with:
trigger: '/test-windows'
windows-build:
name: Windows Build
runs-on: windows-latest
env:
GO111MODULE: "on"
needs: check-optional-tests
if: github.ref == 'refs/heads/main' || needs.check-optional-tests.outputs.trigger-windows == 'true'
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
id: go
- name: Setup docker CLI
run: |
docker version
curl -L -o docker.exe https://github.com/StefanScherer/docker-cli-builder/releases/download/20.10.5/docker.exe
mv -Force ./docker.exe "C:\Program Files\Docker\"
docker version
- name: Checkout code into the Go module directory
uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Test
run: make -f builder.Makefile test
- name: Build
env:
BUILD_TAGS: e2e
run: make -f builder.Makefile cli
- name: E2E Test
run: make e2e-win-ci
- name: ACI e2e Test
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
# need to docker logout on windows nodes, it seems GHActions does a `docker login --user githubactions` specifically on windows nodes
run: |
docker logout
make e2e-aci

查看文件

@ -19,32 +19,11 @@ Once you have the prerequisites installed, you can build the CLI using:
make
```
This will output a CLI for your host machine in `./bin`.
You will then need to make sure that you have the existing Docker CLI in your
`PATH` with the name `com.docker.cli`. A make target is provided to help with
this:
```console
make moby-cli-link
```
This will create a symbolic link from the existing Docker CLI to
`/usr/local/bin` with the name `com.docker.cli`.
This will output a `docker-compose` CLI plugin for your host machine in `./bin`.
You can statically cross compile the CLI for Windows, macOS, and Linux using the
`cross` target.
### Updating the API code
The API provided by the CLI is defined using protobuf. If you make changes to
the `.proto` files in [`protos/`](./protos), you will need to regenerate the API
code:
```console
make protos
```
### Unit tests
To run all of the unit tests, run:
@ -57,69 +36,20 @@ If you need to update a golden file simply do `go test ./... -test.update-golden
### End to end tests
#### Local tests
To run the local end to end tests, run:
To run the end to end tests, run:
```console
make e2e-local
make e2e-compose
```
Note that this requires the CLI to be built and a local Docker Engine to be
running.
#### ACI tests
To run the end to end ACI tests, you will first need to have an Azure account
and have created a service principal. You can create a service principle using
the Azure CLI after you have done a `docker login azure`:
```console
$ az login # az login is not synced with docker azure login, need az login for next step
$ az ad sp create-for-rbac --name 'MyTestServicePrincipal' --sdk-auth
```
You can then run the ACI tests using the `e2e-aci` target with the various
`AZURE_` environment variables set:
```console
AZURE_TENANT_ID="xxx" AZURE_CLIENT_ID="yyy" AZURE_CLIENT_SECRET="yyy" make e2e-aci
```
Running the ACI tests will override your local login and the service principal
credentials use a token that cannot be refreshed automatically.
*Note:* You will need to rerun `docker login azure` if you would like to use the
CLI after running the ACI tests.
You can also run a single ACI test by specifying the test name with the
`E2E_TEST` variable:
```console
AZURE_TENANT_ID="xxx" AZURE_CLIENT_ID="yyy" AZURE_CLIENT_SECRET="yyy" make E2E_TEST=TestContainerRun e2e-aci
```
#### ECS tests
To run the end to end ECS tests, you will need to have an AWS account and have
credentials for it in the `~/.aws/credentials` file.
You can then use the `e2e-ecs` target:
```console
TEST_AWS_PROFILE=myProfile TEST_AWS_REGION=eu-west-3 make e2e-ecs
```
## ACI CI
ACI CI runs E2E tests and needs the same credentials as described above to run these. 3 secrets are defined in github settings, and accessed by the CI job.
To rotate these secrets, run the same `az ad sp create-for-rbac` command and update 3 github secrets with the resulting new service provide info.
Note that this requires a local Docker Engine to be running.
## Releases
To create a new release:
* Check that the CI is green on the main branch for commit you want to release
* Create a new tag of the form vx.y.z, following existing tags, and push the tag
* Run the release Github Actions workflow with a tag of the form vx.y.z following existing tags.
Pushing the tag will automatically create a new release and make binaries for
This will automatically create a new tag, release and make binaries for
Windows, macOS, and Linux available for download on the
[releases page](https://github.com/docker/compose-cli/releases).
[releases page](https://github.com/docker/compose/releases).

查看文件

@ -32,14 +32,6 @@ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
FROM base AS make-protos
ARG PROTOC_GEN_GO_VERSION
RUN go get github.com/golang/protobuf/protoc-gen-go@${PROTOC_GEN_GO_VERSION}
COPY . .
RUN make -f builder.Makefile protos
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION} AS lint-base
FROM base AS lint
ENV CGO_ENABLED=0
COPY --from=lint-base /usr/bin/golangci-lint /usr/bin/golangci-lint
@ -53,30 +45,6 @@ RUN --mount=target=. \
GIT_TAG=${GIT_TAG} \
make -f builder.Makefile lint
FROM base AS import-restrictions-base
RUN go get github.com/docker/import-restrictions
FROM import-restrictions-base AS import-restrictions
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
make -f builder.Makefile import-restrictions
FROM base AS make-cli
ENV CGO_ENABLED=0
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TAGS
ARG GIT_TAG
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH} \
BUILD_TAGS=${BUILD_TAGS} \
GIT_TAG=${GIT_TAG} \
make BINARY=/out/docker -f builder.Makefile cli
FROM base AS make-compose-plugin
ENV CGO_ENABLED=0
ARG TARGETOS
@ -100,13 +68,7 @@ RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
BUILD_TAGS=${BUILD_TAGS} \
GIT_TAG=${GIT_TAG} \
make BINARY=/out/docker COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cross
FROM scratch AS protos
COPY --from=make-protos /compose-cli/cli/server/protos .
FROM scratch AS cli
COPY --from=make-cli /out/* .
make COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cross
FROM scratch AS compose-plugin
COPY --from=make-compose-plugin /out/* .

查看文件

@ -1,106 +0,0 @@
# Mac and Windows installation
The Compose CLI is built into Docker Desktop Edge and Stable.
You can download it from these links:
- [macOS](https://hub.docker.com/editions/community/docker-ce-desktop-mac)
- [Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows)
# Ubuntu Linux installation
The Linux installation script and manual install instructions have been tested
with a fresh install of Ubuntu 20.04.
## Prerequisites
* [Docker 19.03 or later](https://docs.docker.com/engine/install/)
## Install script
You can install the Compose CLI using the install script:
```console
curl -L https://raw.githubusercontent.com/docker/compose-cli/main/scripts/install/install_linux.sh | sh
```
## Manual install
You can download the Compose CLI from [latest release](https://github.com/docker/compose-cli/releases/latest).
You will then need to extract it and make it executable:
```console
$ tar xzf docker-linux-amd64.tar.gz
$ chmod +x docker/docker
```
To enable using the local Docker Engine and to use existing Docker contexts, you
will need to have the existing Docker CLI as `com.docker.cli` somewhere in your
`PATH`. You can do this by creating a symbolic link from the existing Docker
CLI.
```console
ln -s /path/to/existing/docker /directory/in/PATH/com.docker.cli
```
> **Note**: The `PATH` environment variable is a colon separated list of
> directories with priority from left to right. You can view it using
> `echo $PATH`. You can find the path to the existing Docker CLI using
> `which docker`. You may need root permissions to make this link.
On a fresh install of Ubuntu 20.04 with Docker Engine
[already installed](https://docs.docker.com/engine/install/ubuntu/):
```console
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ which docker
/usr/bin/docker
$ sudo ln -s /usr/bin/docker /usr/local/bin/com.docker.cli
```
You can verify that this is working by checking that the new CLI works with the
default context:
```console
$ ./docker/docker --context default ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ echo $?
0
```
To make the Compose CLI your default Docker CLI, you must move it to a directory
in your `PATH` with higher priority than the existing Docker CLI.
Again on a fresh Ubuntu 20.04:
```console
$ which docker
/usr/bin/docker
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ sudo mv docker/docker /usr/local/bin/docker
$ which docker
/usr/local/bin/docker
$ docker version
...
Cloud integration 0.1.6
...
```
# Uninstall
To remove this CLI, you need to remove the binary you downloaded and
`com.docker.cli` from your `PATH`. If you installed using the script, this can
be done as follows:
```console
sudo rm /usr/local/bin/docker /usr/local/bin/com.docker.cli
```
# Testing the install script
To test the install script, from a machine with docker:
```console
docker build -t testclilinux -f scripts/Dockerfile-testInstall scripts
```

查看文件

@ -26,6 +26,7 @@
"gtardif",
"ndeloof"
"chris-crone"
"ulyssessouza"
]
[people]
@ -55,3 +56,8 @@
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"
GitHub = "rumpl"
[people.ulyssessouza]
Name = "Ulysses Souza"
Email = "<ulysses.souza@docker.com"
Github = "ulyssessouza"

查看文件

@ -31,19 +31,9 @@ else
TEST_FLAGS=-run $(E2E_TEST)
endif
all: cli compose-plugin
protos: ## Generate go code from .proto files
@docker build . --target protos \
--output ./cli/server/protos
cli: ## Compile the cli
@docker build . --target cli \
--platform local \
--build-arg BUILD_TAGS=e2e,kube \
--build-arg GIT_TAG=$(GIT_TAG) \
--output ./bin
all: compose-plugin
.PHONY: compose-plugin
compose-plugin: ## Compile the compose cli-plugin
@docker build . --target compose-plugin \
--platform local \
@ -51,101 +41,55 @@ compose-plugin: ## Compile the compose cli-plugin
--build-arg GIT_TAG=$(GIT_TAG) \
--output ./bin
.PHONY: e2e-compose
e2e-compose: ## Run End to end local tests. Set E2E_TEST=TestName to run a single test
gotestsum $(TEST_FLAGS) ./pkg/e2e -- -count=1
e2e-local: ## Run End to end local tests. Set E2E_TEST=TestName to run a single test
gotestsum $(TEST_FLAGS) ./local/e2e/container ./local/e2e/cli-only -- -count=1
e2e-win-ci: ## Run end to end local tests on Windows CI, no Docker for Linux containers available ATM. Set E2E_TEST=TestName to run a single test
go test -count=1 -v $(TEST_FLAGS) ./local/e2e/cli-only
e2e-kube: ## Run End to end Kube tests. Set E2E_TEST=TestName to run a single test
go test -timeout 10m -count=1 -v $(TEST_FLAGS) ./kube/e2e
e2e-aci: ## Run End to end ACI tests. Set E2E_TEST=TestName to run a single test
go test -timeout 15m -count=1 -v $(TEST_FLAGS) ./aci/e2e
e2e-ecs: ## Run End to end ECS tests. Set E2E_TEST=TestName to run a single test
go test -timeout 20m -count=1 -v $(TEST_FLAGS) ./ecs/e2e/ecs ./ecs/e2e/ecs-local
.PHONY: cross
cross: ## Compile the CLI for linux, darwin and windows
@docker build . --target cross \
--build-arg BUILD_TAGS \
--build-arg GIT_TAG=$(GIT_TAG) \
--output ./bin \
.PHONY: test
test: ## Run unit tests
@docker build --progress=plain . \
--build-arg BUILD_TAGS=kube \
--build-arg GIT_TAG=$(GIT_TAG) \
--target test
.PHONY: cache-clear
cache-clear: ## Clear the builder cache
@docker builder prune --force --filter type=exec.cachemount --filter=unused-for=24h
.PHONY: lint
lint: ## run linter(s)
@docker build . \
--build-arg BUILD_TAGS=kube,e2e \
--build-arg GIT_TAG=$(GIT_TAG) \
--target lint
.PHONY: check-dependencies
check-dependencies: ## check dependency updates
go list -u -m -f '{{if not .Indirect}}{{if .Update}}{{.}}{{end}}{{end}}' all
import-restrictions: ## run import-restrictions script
@docker build . \
--target import-restrictions
serve: cli ## start server
@./bin/docker serve --address unix:///tmp/backend.sock
moby-cli-link: ## Create com.docker.cli symlink if does not already exist
ln -s $(MOBY_DOCKER) /usr/local/bin/com.docker.cli
install: ## Link /usr/local/bin/ to current binary
ln -fs $(BINARY_FOLDER)/docker /usr/local/bin/docker
.PHONY: validate-headers
validate-headers: ## Check license header for all files
@docker build . --target check-license-headers
.PHONY: go-mod-tidy
go-mod-tidy: ## Run go mod tidy in a container and output resulting go.mod and go.sum
@docker build . --target go-mod-tidy --output .
.PHONY: validate-go-mod
validate-go-mod: ## Validate go.mod and go.sum are up-to-date
@docker build . --target check-go-mod
validate: validate-go-mod validate-headers ## Validate sources
pre-commit: validate import-restrictions check-dependencies lint cli test e2e-local
build-aci-sidecar: ## build aci sidecar image locally and tag it with make build-aci-sidecar tag=0.1
docker build -t docker/aci-hostnames-sidecar:$(tag) aci/etchosts
publish-aci-sidecar: build-aci-sidecar ## build & publish aci sidecar image with make publish-aci-sidecar tag=0.1
docker pull docker/aci-hostnames-sidecar:$(tag) && echo "Failure: Tag already exists" || docker push docker/aci-hostnames-sidecar:$(tag)
build-ecs-search-sidecar: ## build ecs search sidecar image locally and tag it with make build-ecs-search-sidecar tag=0.1
docker build -t docker/ecs-searchdomain-sidecar:$(tag) ecs/resolv
publish-ecs-search-sidecar: build-ecs-search-sidecar ## build & publish ecs search sidecar image with make publish-ecs-search-sidecar tag=0.1
docker pull docker/ecs-searchdomain-sidecar:$(tag) && echo "Failure: Tag already exists" || docker push docker/ecs-searchdomain-sidecar:$(tag)
build-ecs-secrets-sidecar: ## build ecs secrets sidecar image locally and tag it with make build-ecs-secrets-sidecar tag=0.1
docker build -t docker/ecs-secrets-sidecar:$(tag) ecs/secrets
publish-ecs-secrets-sidecar: build-ecs-secrets-sidecar ## build & publish ecs secrets sidecar image with make publish-ecs-secrets-sidecar tag=0.1
docker pull docker/ecs-secrets-sidecar:$(tag) && echo "Failure: Tag already exists" || docker push docker/ecs-secrets-sidecar:$(tag)
clean-aci-e2e: ## Make sure no ACI tests are currently runnnig in the CI when invoking this. Delete ACI E2E tests resources that might have leaked when ctrl-C E2E tests.
@ echo "Will delete resource groups: "
@ az group list | jq '.[].name' | grep -i E2E-Test
az group list | jq '.[].name' | grep -i E2E-Test | xargs -n1 az group delete -y --no-wait -g
pre-commit: validate check-dependencies lint compose-plugin test e2e-compose
help: ## Show help
@echo Please specify a build target. The choices are:
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
FORCE:
.PHONY: all validate protos cli e2e-local cross test cache-clear lint check-dependencies serve classic-link help clean-aci-e2e go-mod-tidy

4
NOTICE
查看文件

@ -1,4 +1,4 @@
Docker Compose CLI
Copyright 2020 Docker Compose CLI authors
Docker Compose V2
Copyright 2020 Docker Compose authors
This product includes software developed at Docker, Inc. (https://www.docker.com).

查看文件

@ -1,57 +1,63 @@
# Docker Compose CLI
# Docker Compose v2
[![Actions Status](https://github.com/docker/compose-cli/workflows/Continuous%20integration/badge.svg)](https://github.com/docker/compose-cli/actions)
[![Actions Status](https://github.com/docker/compose-cli/workflows/Windows%20CI/badge.svg)](https://github.com/docker/compose-cli/actions)
This Compose CLI tool makes it easy to run Docker containers and Docker Compose applications:
* locally as a command in the docker CLI, using `docker compose ...` comands.
* in the cloud using either Amazon Elastic Container Service
([ECS](https://aws.amazon.com/ecs))
or Microsoft Azure Container Instances
([ACI](https://azure.microsoft.com/services/container-instances))
using the Docker commands you already know.
**Note: Compose CLI is released under the 1.x tag, until "Compose v2" gets a new home**
![Docker Compose](logo.png?raw=true "Docker Compose Logo")
## Compose v2 (a.k.a "Local Docker Compose")
Docker Compose is a tool for running multi-container applications on Docker
defined using the [Compose file format](https://compose-spec.io).
A Compose file is used to define how the one or more containers that make up
your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: `docker-compose up`.
The `docker compose` local command is the next major version for docker-compose, and it supports the same commands and flags, in order to be used as a drop-in replacement.
[Here](https://github.com/docker/compose-cli/issues/1283) is a checklist of docker-compose commands and flags that are implemented in `docker compose`.
# Where to get Docker Compose
This `docker compose` local command :
* has a better integration with the rest of the docker ecosystem (being written in go, it's easier to share functionality with the Docker CLI and other Docker libraries)
* is quicker and uses more parallelism to run multiple tasks in parallel. It also uses buildkit by default
* includes additional commands, like `docker compose ls` to list current compose projects
### Windows and macOS
**Note: Compose v2 is released under the 2.x tag, until "Compose v2" gets a new home**
Docker Compose is included in
[Docker Desktop](https://www.docker.com/products/docker-desktop)
for Windows and macOS.
Compose v2 can be installed manually as a CLI plugin, by downloading latest v2.x release from https://github.com/docker/compose-cli/releases for your architecture and move into `~/.docker/cli-plugins/docker-compose`
### Linux
## Getting started
You can download Docker Compose binaries from the
[release page](https://github.com/docker/compose/releases) on this repository.
To get started with Compose CLI, all you need is:
Copy the relevant binary for your OS under `$HOME/.docker/cli-plugins/docker-compose`
(might require to make the downloaded file executable with `chmod +x`)
* macOS, Windows, or Windows WSL2: The current release of
[Docker Desktop](https://www.docker.com/products/docker-desktop)
* Linux:
[Install script](INSTALL.md)
* An [AWS](https://aws.amazon.com) or [Azure](https://azure.microsoft.com)
account in order to use the Compose Cloud integration
Quick Start
-----------
Please create [issues](https://github.com/docker/compose-cli/issues) to leave feedback.
Using Docker Compose is basically a three-step process:
1. Define your app's environment with a `Dockerfile` so it can be
reproduced anywhere.
2. Define the services that make up your app in `docker-compose.yml` so
they can be run together in an isolated environment.
3. Lastly, run `docker-compose up` and Compose will start and run your entire
app.
## Examples
A Compose file looks like this:
* ECS: [Deploying Wordpress to the cloud](https://www.docker.com/blog/deploying-wordpress-to-the-cloud/)
* ACI: [Deploying a Minecraft server to the cloud](https://www.docker.com/blog/deploying-a-minecraft-docker-server-to-the-cloud/)
* ACI: [Setting Up Cloud Deployments Using Docker, Azure and Github Actions](https://www.docker.com/blog/setting-up-cloud-deployments-using-docker-azure-and-github-actions/)
```yaml
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
redis:
image: redis
```
## Development
Contributing
------------
See the instructions in [BUILDING.md](BUILDING.md) for how to build the CLI and
run its tests; including the end to end tests for local containers, ACI, and
ECS.
The guide also includes instructions for releasing the CLI.
Want to help develop Docker Compose? Check out our
[contributing documentation](CONTRIBUTING.md).
Before contributing, please read the [contribution guidelines](CONTRIBUTING.md)
which includes conventions used in this project.
If you find an issue, please report it on the
[issue tracker](https://github.com/docker/compose/issues/new/choose).

查看文件

@ -1,388 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to"
tm "github.com/buger/goterm"
"github.com/compose-spec/compose-go/types"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/pkg/progress"
)
func createACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return errors.Wrapf(err, "cannot get container group client")
}
// Check if the container group already exists
_, err = containerGroupsClient.Get(ctx, aciContext.ResourceGroup, *groupDefinition.Name)
if err != nil {
if err, ok := err.(autorest.DetailedError); ok {
if err.StatusCode != http.StatusNotFound {
return err
}
} else {
return err
}
} else {
return fmt.Errorf("container group %q already exists", *groupDefinition.Name)
}
return createOrUpdateACIContainers(ctx, aciContext, groupDefinition)
}
func autocreateFileshares(ctx context.Context, project *types.Project) error {
clt, err := client.New(ctx)
if err != nil {
return err
}
for _, v := range project.Volumes {
if v.Driver != convert.AzureFileDriverName {
return fmt.Errorf("cannot use ACI volume, required driver is %q, found %q", convert.AzureFileDriverName, v.Driver)
}
shareName, ok := v.DriverOpts[convert.VolumeDriveroptsShareNameKey]
if !ok {
return fmt.Errorf("cannot retrieve fileshare name for Azure file share")
}
accountName, ok := v.DriverOpts[convert.VolumeDriveroptsAccountNameKey]
if !ok {
return fmt.Errorf("cannot retrieve account name for Azure file share")
}
_, err = clt.VolumeService().Inspect(ctx, fmt.Sprintf("%s/%s", accountName, shareName))
if err != nil { // Not found, autocreate fileshare
if !api.IsNotFoundError(err) {
return err
}
aciVolumeOpts := &VolumeCreateOptions{
Account: accountName,
}
if _, err = clt.VolumeService().Create(ctx, shareName, aciVolumeOpts); err != nil {
return err
}
}
}
return nil
}
func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
w := progress.ContextWriter(ctx)
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return errors.Wrapf(err, "cannot get container group client")
}
groupDisplay := "Group " + *groupDefinition.Name
w.Event(progress.CreatingEvent(groupDisplay))
future, err := containerGroupsClient.CreateOrUpdate(
ctx,
aciContext.ResourceGroup,
*groupDefinition.Name,
groupDefinition,
)
if err != nil {
w.Event(progress.ErrorEvent(groupDisplay))
return err
}
w.Event(progress.CreatedEvent(groupDisplay))
for _, c := range *groupDefinition.Containers {
if c.Name != nil && *c.Name != convert.ComposeDNSSidecarName {
w.Event(progress.CreatingEvent(*c.Name))
}
}
err = future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
if err != nil {
return err
}
for _, c := range *groupDefinition.Containers {
if c.Name != nil && *c.Name != convert.ComposeDNSSidecarName {
w.Event(progress.CreatedEvent(*c.Name))
}
}
return err
}
func getACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) {
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return containerinstance.ContainerGroup{}, fmt.Errorf("cannot get container group client: %v", err)
}
return containerGroupsClient.Get(ctx, aciContext.ResourceGroup, containerGroupName)
}
func getACIContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) {
groupsClient, err := login.NewContainerGroupsClient(subscriptionID)
if err != nil {
return nil, err
}
var containerGroups []containerinstance.ContainerGroup
result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup)
if err != nil {
return nil, err
}
for result.NotDone() {
containerGroups = append(containerGroups, result.Values()...)
if err := result.NextWithContext(ctx); err != nil {
return nil, err
}
}
var groups []containerinstance.ContainerGroup
for _, group := range containerGroups {
group, err := groupsClient.Get(ctx, resourceGroup, *group.Name)
if err != nil {
return nil, err
}
groups = append(groups, group)
}
return groups, nil
}
func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) {
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return containerinstance.ContainerGroup{}, fmt.Errorf("cannot get container group client: %v", err)
}
result, err := containerGroupsClient.Delete(ctx, aciContext.ResourceGroup, containerGroupName)
if err != nil {
return containerinstance.ContainerGroup{}, fmt.Errorf("cannot delete container group: %v", err)
}
return result.Result(containerGroupsClient)
}
func stopACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) error {
containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return fmt.Errorf("cannot get container group client: %v", err)
}
result, err := containerGroupsClient.Stop(ctx, aciContext.ResourceGroup, containerGroupName)
if result.IsHTTPStatus(http.StatusNotFound) {
return api.ErrNotFound
}
return err
}
func execACIContainer(ctx context.Context, aciContext store.AciContext, command, containerGroup string, containerName string) (c containerinstance.ContainerExecResponse, err error) {
containerClient, err := login.NewContainerClient(aciContext.SubscriptionID)
if err != nil {
return c, errors.Wrapf(err, "cannot get container client")
}
rows, cols := getTermSize()
containerExecRequest := containerinstance.ContainerExecRequest{
Command: to.StringPtr(command),
TerminalSize: &containerinstance.ContainerExecRequestTerminalSize{
Rows: rows,
Cols: cols,
},
}
return containerClient.ExecuteCommand(
ctx,
aciContext.ResourceGroup,
containerGroup,
containerName,
containerExecRequest)
}
func getTermSize() (*int32, *int32) {
rows := tm.Height()
cols := tm.Width()
return to.Int32Ptr(int32(rows)), to.Int32Ptr(int32(cols))
}
func exec(ctx context.Context, address string, password string, request containers.ExecRequest) error {
conn, _, _, err := ws.DefaultDialer.Dial(ctx, address)
if err != nil {
return err
}
err = wsutil.WriteClientMessage(conn, ws.OpText, []byte(password))
if err != nil {
return err
}
downstreamChannel := make(chan error, 10)
upstreamChannel := make(chan error, 10)
go func() {
for {
msg, _, err := wsutil.ReadServerData(conn)
if err != nil {
if err == io.EOF {
downstreamChannel <- nil
return
}
downstreamChannel <- err
return
}
fmt.Fprint(request.Stdout, string(msg))
}
}()
if request.Interactive {
go func() {
for {
// We send each byte, byte-per-byte over the
// websocket because the console is in raw mode
buffer := make([]byte, 1)
n, err := request.Stdin.Read(buffer)
if err != nil {
if err == io.EOF {
upstreamChannel <- nil
return
}
upstreamChannel <- err
return
}
if n > 0 {
err := wsutil.WriteClientMessage(conn, ws.OpText, buffer)
if err != nil {
upstreamChannel <- err
return
}
}
}
}()
}
for {
select {
case err := <-downstreamChannel:
return errors.Wrap(err, "failed to read input from container")
case err := <-upstreamChannel:
return errors.Wrap(err, "failed to send input to container")
}
}
}
func getACIContainerLogs(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string, tail *int32) (string, error) {
containerClient, err := login.NewContainerClient(aciContext.SubscriptionID)
if err != nil {
return "", errors.Wrapf(err, "cannot get container client")
}
logs, err := containerClient.ListLogs(ctx, aciContext.ResourceGroup, containerGroupName, containerName, tail)
if err != nil {
return "", fmt.Errorf("cannot get container logs: %v", err)
}
if logs.Content == nil {
return "", nil
}
return *logs.Content, err
}
func streamLogs(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string, req containers.LogsRequest) error {
numLines := 0
previousLogLines := ""
firstDisplay := true // optimization to exit sooner in cases like docker run hello-world, do not wait another 2 secs.
for {
select {
case <-ctx.Done():
return nil
default:
logs, err := getACIContainerLogs(ctx, aciContext, containerGroupName, containerName, nil)
if err != nil {
return err
}
logLines := strings.Split(logs, "\n")
currentOutput := len(logLines)
// Note: a backend should not do this normally, this breaks the log
// streaming over gRPC but this is the only thing we can do with
// the kind of logs ACI is giving us. Hopefully Azue will give us
// a real logs streaming api soon.
b := aec.EmptyBuilder
b = b.Up(uint(numLines))
fmt.Fprint(req.Writer, b.Column(0).ANSI)
numLines = getBacktrackLines(logLines, req.Width)
for i := 0; i < currentOutput-1; i++ {
fmt.Fprintln(req.Writer, logLines[i])
}
if (firstDisplay || previousLogLines == logs) && !isContainerRunning(ctx, aciContext, containerGroupName, containerName) {
return nil
}
firstDisplay = false
previousLogLines = logs
select {
case <-ctx.Done():
return nil
case <-time.After(2 * time.Second):
}
}
}
}
func isContainerRunning(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string) bool {
group, err := getACIContainerGroup(ctx, aciContext, containerGroupName)
if err != nil {
return false // group has disappeared
}
for _, container := range *group.Containers {
if *container.Name == containerName {
if convert.GetStatus(container, group) == convert.StatusRunning {
return true
}
}
}
return false
}
func getBacktrackLines(lines []string, terminalWidth int) int {
if terminalWidth == 0 { // no terminal width has been set, do not divide by zero
return len(lines)
}
numLines := 0
for i := 0; i < len(lines)-1; i++ {
numLines++
if len(lines[i]) > terminalWidth {
numLines += len(lines[i]) / terminalWidth
}
}
return numLines
}

查看文件

@ -1,28 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"testing"
"gotest.tools/v3/assert"
)
func TestGetLinesWritten(t *testing.T) {
assert.Equal(t, 0, getBacktrackLines([]string{"Hello"}, 10))
assert.Equal(t, 3, getBacktrackLines([]string{"Hello", "world"}, 2))
}

查看文件

@ -1,164 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/backend"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/resources"
"github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/api/cloud"
apicontext "github.com/docker/compose-cli/api/context"
"github.com/docker/compose-cli/api/context/store"
)
const (
backendType = store.AciContextType
singleContainerTag = "docker-single-container"
composeContainerTag = "docker-compose-application"
dockerVolumeTag = "docker-volume"
composeContainerSeparator = "_"
)
// LoginParams azure login options
type LoginParams struct {
TenantID string
ClientID string
ClientSecret string
CloudName string
}
// Validate returns an error if options are not used properly
func (opts LoginParams) Validate() error {
if opts.ClientID != "" || opts.ClientSecret != "" {
if opts.ClientID == "" || opts.ClientSecret == "" || opts.TenantID == "" {
return errors.New("for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
}
}
return nil
}
func init() {
backend.Register(backendType, backendType, service, getCloudService)
}
func service() (backend.Service, error) {
contextStore := store.Instance()
currentContext := apicontext.Current()
var aciContext store.AciContext
if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
return nil, err
}
return getAciAPIService(aciContext), nil
}
func getCloudService() (cloud.Service, error) {
service, err := login.NewAzureLoginService()
if err != nil {
return nil, err
}
return &aciCloudService{
loginService: service,
}, nil
}
func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
containerService := newContainerService(aciCtx)
composeService := newComposeService(aciCtx)
return &aciAPIService{
aciContainerService: &containerService,
aciComposeService: &composeService,
aciVolumeService: &aciVolumeService{
aciContext: aciCtx,
},
aciResourceService: &aciResourceService{
aciContext: aciCtx,
},
}
}
type aciAPIService struct {
*aciContainerService
*aciComposeService
*aciVolumeService
*aciResourceService
}
func (a *aciAPIService) ContainerService() containers.Service {
return a.aciContainerService
}
func (a *aciAPIService) ComposeService() api.Service {
return a.aciComposeService
}
func (a *aciAPIService) SecretsService() secrets.Service {
// Not implemented on ACI
// Secrets are created and mounted in the container at it's creation and not stored on ACI
return nil
}
func (a *aciAPIService) VolumeService() volumes.Service {
return a.aciVolumeService
}
func (a *aciAPIService) ResourceService() resources.Service {
return a.aciResourceService
}
func getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
containerID := *group.Name + composeContainerSeparator + *container.Name
if _, ok := group.Tags[singleContainerTag]; ok {
containerID = *group.Name
}
return containerID
}
func isContainerVisible(container containerinstance.Container, group containerinstance.ContainerGroup, showAll bool) bool {
return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != convert.StatusRunning)
}
func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
if groupDefinition.Tags == nil {
groupDefinition.Tags = make(map[string]*string, 1)
}
groupDefinition.Tags[tagName] = to.StringPtr(tagName)
}
func getGroupAndContainerName(containerID string) (string, string) {
tokens := strings.Split(containerID, composeContainerSeparator)
groupName := tokens[0]
containerName := groupName
if len(tokens) > 1 {
containerName = tokens[len(tokens)-1]
groupName = containerID[:len(containerID)-(len(containerName)+1)]
}
return groupName, containerName
}

查看文件

@ -1,165 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/containers"
"golang.org/x/oauth2"
)
func TestGetContainerName(t *testing.T) {
group, container := getGroupAndContainerName("docker1234")
assert.Equal(t, group, "docker1234")
assert.Equal(t, container, "docker1234")
group, container = getGroupAndContainerName("compose_service1")
assert.Equal(t, group, "compose")
assert.Equal(t, container, "service1")
group, container = getGroupAndContainerName("compose_stack_service1")
assert.Equal(t, group, "compose_stack")
assert.Equal(t, container, "service1")
}
func TestErrorMessageDeletingContainerFromComposeApplication(t *testing.T) {
service := aciContainerService{}
err := service.Delete(context.TODO(), "compose-app_service1", containers.DeleteRequest{Force: false})
assert.Error(t, err, "cannot delete service \"service1\" from compose application \"compose-app\", you can delete the entire compose app with docker compose down --project-name compose-app")
}
func TestErrorMessageRunSingleContainerNameWithComposeSeparator(t *testing.T) {
service := aciContainerService{}
err := service.Run(context.TODO(), containers.ContainerConfig{ID: "container_name"})
assert.Error(t, err, "invalid container name. ACI container name cannot include \"_\"")
}
func TestVerifyCommand(t *testing.T) {
err := verifyExecCommand("command") // Command without an argument
assert.NilError(t, err)
err = verifyExecCommand("command argument") // Command with argument
assert.Error(t, err, "ACI exec command does not accept arguments to the command. "+
"Only the binary should be specified")
}
func TestLoginParamsValidate(t *testing.T) {
err := LoginParams{
ClientID: "someID",
}.Validate()
assert.Error(t, err, "for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
err = LoginParams{
ClientSecret: "someSecret",
}.Validate()
assert.Error(t, err, "for Service Principal login, 3 options must be specified: --client-id, --client-secret and --tenant-id")
err = LoginParams{}.Validate()
assert.NilError(t, err)
err = LoginParams{
TenantID: "tenant",
}.Validate()
assert.NilError(t, err)
}
func TestLoginServicePrincipal(t *testing.T) {
loginService := mockLoginService{}
loginService.On("LoginServicePrincipal", "someID", "secret", "tenant", "someCloud").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(context.Background(), LoginParams{
ClientID: "someID",
ClientSecret: "secret",
TenantID: "tenant",
CloudName: "someCloud",
})
assert.NilError(t, err)
}
func TestLoginWithTenant(t *testing.T) {
loginService := mockLoginService{}
ctx := context.Background()
loginService.On("Login", ctx, "tenant", "someCloud").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(ctx, LoginParams{
TenantID: "tenant",
CloudName: "someCloud",
})
assert.NilError(t, err)
}
func TestLoginWithoutTenant(t *testing.T) {
loginService := mockLoginService{}
ctx := context.Background()
loginService.On("Login", ctx, "", "someCloud").Return(nil)
loginBackend := aciCloudService{
loginService: &loginService,
}
err := loginBackend.Login(ctx, LoginParams{
CloudName: "someCloud",
})
assert.NilError(t, err)
}
type mockLoginService struct {
mock.Mock
}
func (s *mockLoginService) Login(ctx context.Context, requestedTenantID string, cloudEnvironment string) error {
args := s.Called(ctx, requestedTenantID, cloudEnvironment)
return args.Error(0)
}
func (s *mockLoginService) LoginServicePrincipal(clientID string, clientSecret string, tenantID string, cloudEnvironment string) error {
args := s.Called(clientID, clientSecret, tenantID, cloudEnvironment)
return args.Error(0)
}
func (s *mockLoginService) Logout(ctx context.Context) error {
args := s.Called(ctx)
return args.Error(0)
}
func (s *mockLoginService) GetTenantID() (string, error) {
args := s.Called()
return args.String(0), args.Error(1)
}
func (s *mockLoginService) GetCloudEnvironment() (login.CloudEnvironment, error) {
args := s.Called()
return args.Get(0).(login.CloudEnvironment), args.Error(1)
}
func (s *mockLoginService) GetValidToken() (oauth2.Token, string, error) {
args := s.Called()
return args.Get(0).(oauth2.Token), args.String(1), args.Error(2)
}

查看文件

@ -1,53 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/login"
)
type aciCloudService struct {
loginService login.AzureLoginService
}
func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
opts, ok := params.(LoginParams)
if !ok {
return errors.New("could not read Azure LoginParams struct from generic parameter")
}
if opts.CloudName == "" {
opts.CloudName = login.AzurePublicCloudName
}
if opts.ClientID != "" {
return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID, opts.CloudName)
}
return cs.loginService.Login(ctx, opts.TenantID, opts.CloudName)
}
func (cs *aciCloudService) Logout(ctx context.Context) error {
return cs.loginService.Logout(ctx)
}
func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
contextHelper := newContextCreateHelper()
createOpts := params.(ContextParams)
return contextHelper.createContextData(ctx, createOpts)
}

查看文件

@ -1,266 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"net/http"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/pkg/progress"
"github.com/docker/compose-cli/utils/formatter"
)
type aciComposeService struct {
ctx store.AciContext
storageLogin login.StorageLoginImpl
}
func newComposeService(ctx store.AciContext) aciComposeService {
return aciComposeService{
ctx: ctx,
storageLogin: login.StorageLoginImpl{AciContext: ctx},
}
}
func (cs *aciComposeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Create(ctx context.Context, project *types.Project, opts api.CreateOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, options api.StartOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Pause(ctx context.Context, project string, options api.PauseOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) UnPause(ctx context.Context, project string, options api.PauseOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Copy(ctx context.Context, project *types.Project, options api.CopyOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error {
return progress.Run(ctx, func(ctx context.Context) error {
return cs.up(ctx, project)
})
}
func (cs *aciComposeService) up(ctx context.Context, project *types.Project) error {
logrus.Debugf("Up on project with name %q", project.Name)
if err := autocreateFileshares(ctx, project); err != nil {
return err
}
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project, cs.storageLogin)
if err != nil {
return err
}
addTag(&groupDefinition, composeContainerTag)
return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs aciComposeService) warnKeepVolumeOnDown(ctx context.Context, projectName string) error {
cgClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
cg, err := cgClient.Get(ctx, cs.ctx.ResourceGroup, projectName)
if err != nil {
return err
}
if cg.Volumes == nil {
return nil
}
for _, v := range *cg.Volumes {
if v.AzureFile == nil || v.AzureFile.StorageAccountName == nil || v.AzureFile.ShareName == nil {
continue
}
fmt.Printf("WARNING: fileshare \"%s/%s\" will NOT be deleted. Use 'docker volume rm' if you want to delete this volume\n",
*v.AzureFile.StorageAccountName, *v.AzureFile.ShareName)
}
return nil
}
func (cs *aciComposeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
if options.Volumes {
return errors.Wrap(api.ErrNotImplemented, "--volumes option is not supported on ACI")
}
if options.Images != "" {
return errors.Wrap(api.ErrNotImplemented, "--rmi option is not supported on ACI")
}
return progress.Run(ctx, func(ctx context.Context) error {
logrus.Debugf("Down on project with name %q", projectName)
if err := cs.warnKeepVolumeOnDown(ctx, projectName); err != nil {
return err
}
cg, err := deleteACIContainerGroup(ctx, cs.ctx, projectName)
if err != nil {
return err
}
if cg.IsHTTPStatus(http.StatusNoContent) {
return api.ErrNotFound
}
return err
})
}
func (cs *aciComposeService) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return nil, err
}
group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, projectName)
if err != nil {
return nil, err
}
if group.Containers == nil || len(*group.Containers) == 0 {
return nil, fmt.Errorf("no containers found in ACI container group %s", projectName)
}
res := []api.ContainerSummary{}
for _, container := range *group.Containers {
if isContainerVisible(container, group, false) {
continue
}
var publishers []api.PortPublisher
urls := formatter.PortsToStrings(convert.ToPorts(group.IPAddress, *container.Ports), convert.FQDN(group, cs.ctx.Location))
for i, p := range *container.Ports {
publishers = append(publishers, api.PortPublisher{
URL: urls[i],
TargetPort: int(*p.Port),
PublishedPort: int(*p.Port),
Protocol: string(p.Protocol),
})
}
id := getContainerID(group, container)
res = append(res, api.ContainerSummary{
ID: id,
Name: id,
Project: projectName,
Service: *container.Name,
State: convert.GetStatus(container, group),
Publishers: publishers,
})
}
return res, nil
}
func (cs *aciComposeService) List(ctx context.Context, opts api.ListOptions) ([]api.Stack, error) {
containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return nil, err
}
var stacks []api.Stack
for _, group := range containerGroups {
if _, found := group.Tags[composeContainerTag]; !found {
continue
}
state := api.RUNNING
for _, container := range *group.ContainerGroupProperties.Containers {
containerState := convert.GetStatus(container, group)
if containerState != api.RUNNING {
state = containerState
break
}
}
stacks = append(stacks, api.Stack{
ID: *group.ID,
Name: *group.Name,
Status: state,
})
}
return stacks, nil
}
func (cs *aciComposeService) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, options api.ConvertOptions) ([]byte, error) {
return nil, api.ErrNotImplemented
}
func (cs *aciComposeService) Kill(ctx context.Context, project *types.Project, options api.KillOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
return 0, api.ErrNotImplemented
}
func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options api.RemoveOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
return 0, api.ErrNotImplemented
}
func (cs *aciComposeService) Top(ctx context.Context, projectName string, services []string) ([]api.ContainerProcSummary, error) {
return nil, api.ErrNotImplemented
}
func (cs *aciComposeService) Events(ctx context.Context, project string, options api.EventsOptions) error {
return api.ErrNotImplemented
}
func (cs *aciComposeService) Port(ctx context.Context, project string, service string, port int, options api.PortOptions) (string, int, error) {
return "", 0, api.ErrNotImplemented
}
func (cs *aciComposeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
return nil, api.ErrNotImplemented
}

查看文件

@ -1,264 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
)
type aciContainerService struct {
ctx store.AciContext
storageLogin login.StorageLoginImpl
}
func newContainerService(ctx store.AciContext) aciContainerService {
return aciContainerService{
ctx: ctx,
storageLogin: login.StorageLoginImpl{AciContext: ctx},
}
}
func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
if err != nil {
return nil, err
}
res := []containers.Container{}
for _, group := range containerGroups {
if group.Containers == nil || len(*group.Containers) == 0 {
return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
}
for _, container := range *group.Containers {
if isContainerVisible(container, group, all) {
continue
}
c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container, cs.ctx.Location)
res = append(res, c)
}
}
return res, nil
}
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
if strings.Contains(r.ID, composeContainerSeparator) {
return fmt.Errorf("invalid container name. ACI container name cannot include %q", composeContainerSeparator)
}
project, err := convert.ContainerToComposeProject(r)
if err != nil {
return err
}
logrus.Debugf("Running container %q with name %q", r.Image, r.ID)
groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project, cs.storageLogin)
if err != nil {
return err
}
addTag(&groupDefinition, singleContainerTag)
return createACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
if err != nil {
var aerr autorest.DetailedError
if ok := errors.As(err, &aerr); ok {
if aerr.StatusCode == http.StatusNotFound {
return api.ErrNotFound
}
}
return err
}
return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
}
func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
if timeout != nil && *timeout != uint32(0) {
return fmt.Errorf("the ACI integration does not support setting a timeout to stop a container before killing it")
}
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
return stopACIContainerGroup(ctx, cs.ctx, groupName)
}
func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead
}
func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
err := verifyExecCommand(request.Command)
if err != nil {
return err
}
groupName, containerAciName := getGroupAndContainerName(name)
containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
if err != nil {
return err
}
return exec(
context.Background(),
*containerExecResponse.WebSocketURI,
*containerExecResponse.Password,
request,
)
}
func verifyExecCommand(command string) error {
tokens := strings.Split(command, " ")
if len(tokens) > 1 {
return errors.New("ACI exec command does not accept arguments to the command. " +
"Only the binary should be specified")
}
return nil
}
func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
groupName, containerAciName := getGroupAndContainerName(containerName)
var tail *int32
if req.Follow {
return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
}
if req.Tail != "all" {
reqTail, err := strconv.Atoi(req.Tail)
if err != nil {
return err
}
i32 := int32(reqTail)
tail = &i32
}
logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
if err != nil {
return err
}
_, err = fmt.Fprint(req.Writer, logs)
return err
}
func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
groupName, containerName := getGroupAndContainerName(containerID)
if groupName != containerID {
msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
return fmt.Errorf(msg, containerName, groupName, groupName)
}
if !request.Force {
containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return err
}
cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
if err != nil {
if cg.StatusCode == http.StatusNotFound {
return api.ErrNotFound
}
return err
}
for _, container := range *cg.Containers {
status := convert.GetStatus(container, cg)
if status == convert.StatusRunning {
return api.ErrForbidden
}
}
}
cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
// Delete returns `StatusNoContent` if the group is not found
if cg.IsHTTPStatus(http.StatusNoContent) {
return api.ErrNotFound
}
if err != nil {
return err
}
return err
}
func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
groupName, containerName := getGroupAndContainerName(containerID)
if containerID == "" {
return containers.Container{}, errors.New("cannot inspect empty container ID")
}
cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
if err != nil {
return containers.Container{}, err
}
if cg.IsHTTPStatus(http.StatusNoContent) || cg.ContainerGroupProperties == nil || cg.ContainerGroupProperties.Containers == nil {
return containers.Container{}, api.ErrNotFound
}
var cc containerinstance.Container
var found = false
for _, c := range *cg.Containers {
if to.String(c.Name) == containerName {
cc = c
found = true
break
}
}
if !found {
return containers.Container{}, api.ErrNotFound
}
return convert.ContainerGroupToContainer(containerID, cg, cc, cs.ctx.Location), nil
}

查看文件

@ -1,184 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"os"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources"
"github.com/hashicorp/go-uuid"
"github.com/pkg/errors"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/pkg/prompt"
)
// ContextParams options for creating ACI context
type ContextParams struct {
Description string
Location string
SubscriptionID string
ResourceGroup string
}
// ErrSubscriptionNotFound is returned when a required subscription is not found
var ErrSubscriptionNotFound = errors.Wrapf(api.ErrNotFound, "subscription")
// IsSubscriptionNotFoundError returns true if the unwrapped error is IsSubscriptionNotFoundError
func IsSubscriptionNotFoundError(err error) bool {
return errors.Is(err, ErrSubscriptionNotFound)
}
type contextCreateACIHelper struct {
selector prompt.UI
resourceGroupHelper ResourceGroupHelper
}
func newContextCreateHelper() contextCreateACIHelper {
return contextCreateACIHelper{
selector: prompt.User{},
resourceGroupHelper: aciResourceGroupHelperImpl{},
}
}
func (helper contextCreateACIHelper) createContextData(ctx context.Context, opts ContextParams) (interface{}, string, error) {
subs, err := helper.resourceGroupHelper.GetSubscriptionIDs(ctx)
if err != nil {
return nil, "", err
}
subscriptionID := ""
if opts.SubscriptionID != "" {
for _, sub := range subs {
if *sub.SubscriptionID == opts.SubscriptionID {
subscriptionID = opts.SubscriptionID
}
}
if subscriptionID == "" {
return nil, "", ErrSubscriptionNotFound
}
} else {
subscriptionID, err = helper.chooseSub(subs)
if err != nil {
return nil, "", err
}
}
var group resources.Group
if opts.ResourceGroup != "" {
group, err = helper.resourceGroupHelper.GetGroup(ctx, subscriptionID, opts.ResourceGroup)
if err != nil {
return nil, "", errors.Wrapf(err, "Could not find resource group %q", opts.ResourceGroup)
}
} else {
groups, err := helper.resourceGroupHelper.ListGroups(ctx, subscriptionID)
if err != nil {
return nil, "", err
}
group, err = helper.chooseGroup(ctx, subscriptionID, opts, groups)
if err != nil {
return nil, "", err
}
}
location := opts.Location
if opts.Location == "" {
location = *group.Location
}
description := fmt.Sprintf("%s@%s", *group.Name, location)
if opts.Description != "" {
description = fmt.Sprintf("%s (%s)", opts.Description, description)
}
return store.AciContext{
SubscriptionID: subscriptionID,
Location: location,
ResourceGroup: *group.Name,
}, description, nil
}
func (helper contextCreateACIHelper) createGroup(ctx context.Context, subscriptionID, location string) (resources.Group, error) {
if location == "" {
location = "eastus"
}
gid, err := uuid.GenerateUUID()
if err != nil {
return resources.Group{}, err
}
g, err := helper.resourceGroupHelper.CreateOrUpdate(ctx, subscriptionID, gid, resources.Group{
Location: &location,
})
if err != nil {
return resources.Group{}, err
}
fmt.Printf("Resource group %q (%s) created\n", *g.Name, *g.Location)
return g, nil
}
func (helper contextCreateACIHelper) chooseGroup(ctx context.Context, subscriptionID string, opts ContextParams, groups []resources.Group) (resources.Group, error) {
groupNames := []string{"create a new resource group"}
for _, g := range groups {
groupNames = append(groupNames, fmt.Sprintf("%s (%s)", *g.Name, *g.Location))
}
group, err := helper.selector.Select("Select a resource group", groupNames)
if err != nil {
if err == terminal.InterruptErr {
return resources.Group{}, api.ErrCanceled
}
return resources.Group{}, err
}
if group == 0 {
return helper.createGroup(ctx, subscriptionID, opts.Location)
}
return groups[group-1], nil
}
func (helper contextCreateACIHelper) chooseSub(subs []subscription.Model) (string, error) {
if len(subs) == 1 {
sub := subs[0]
fmt.Println("Using only available subscription : " + display(sub))
return *sub.SubscriptionID, nil
}
var options []string
for _, sub := range subs {
options = append(options, display(sub))
}
selected, err := helper.selector.Select("Select a subscription ID", options)
if err != nil {
if err == terminal.InterruptErr {
os.Exit(0)
}
return "", err
}
return *subs[selected].SubscriptionID, nil
}
func display(sub subscription.Model) string {
return fmt.Sprintf("%s (%s)", *sub.DisplayName, *sub.SubscriptionID)
}

查看文件

@ -1,272 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"github.com/docker/compose-cli/api/context/store"
)
type contextMocks struct {
userPrompt *mockUserPrompt
resourceGroupHelper *MockResourceGroupHelper
contextCreateHelper contextCreateACIHelper
}
func testContextMocks() contextMocks {
mockUserPrompt := &mockUserPrompt{}
mockResourceGroupHelper := &MockResourceGroupHelper{}
contextCreateHelper := contextCreateACIHelper{
mockUserPrompt,
mockResourceGroupHelper,
}
return contextMocks{mockUserPrompt, mockResourceGroupHelper, contextCreateHelper}
}
func TestCreateSpecifiedSubscriptionAndGroup(t *testing.T) {
ctx := context.TODO()
opts := options("1234", "myResourceGroup")
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
m.resourceGroupHelper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(group("myResourceGroup", "eastus"), nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "myResourceGroup@eastus")
assert.DeepEqual(t, data, aciContext("1234", "myResourceGroup", "eastus"))
}
func TestErrorOnNonExistentResourceGroup(t *testing.T) {
ctx := context.TODO()
opts := options("1234", "myResourceGroup")
notFoundError := errors.New(`Not Found: "myResourceGroup"`)
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
m.resourceGroupHelper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(resources.Group{}, notFoundError)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.Assert(t, cmp.Nil(data))
assert.Equal(t, description, "")
assert.Error(t, err, "Could not find resource group \"myResourceGroup\": Not Found: \"myResourceGroup\"")
}
func TestErrorOnNonExistentSubscriptionID(t *testing.T) {
ctx := context.TODO()
opts := options("otherSubscription", "myResourceGroup")
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.Assert(t, cmp.Nil(data))
assert.Equal(t, description, "")
assert.Assert(t, err == ErrSubscriptionNotFound)
}
func TestCreateNewResourceGroup(t *testing.T) {
ctx := context.TODO()
opts := options("1234", "")
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
m.resourceGroupHelper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(group("myResourceGroup", "eastus"), nil)
selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
m.userPrompt.On("Select", "Select a resource group", selectOptions).Return(0, nil)
m.resourceGroupHelper.On("CreateOrUpdate", ctx, "1234", mock.AnythingOfType("string"), mock.AnythingOfType("resources.Group")).Return(group("newResourceGroup", "eastus"), nil)
m.resourceGroupHelper.On("ListGroups", ctx, "1234").Return([]resources.Group{
group("group1", "eastus"),
group("group2", "westeurope"),
}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "newResourceGroup@eastus")
assert.DeepEqual(t, data, aciContext("1234", "newResourceGroup", "eastus"))
}
func TestCreateNewResourceGroupWithSpecificLocation(t *testing.T) {
ctx := context.TODO()
opts := options("1234", "")
opts.Location = "eastus2"
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
m.resourceGroupHelper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(group("myResourceGroup", "eastus"), nil)
selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
m.userPrompt.On("Select", "Select a resource group", selectOptions).Return(0, nil)
m.resourceGroupHelper.On("CreateOrUpdate", ctx, "1234", mock.AnythingOfType("string"), mock.AnythingOfType("resources.Group")).Return(group("newResourceGroup", "eastus"), nil)
m.resourceGroupHelper.On("ListGroups", ctx, "1234").Return([]resources.Group{
group("group1", "eastus"),
group("group2", "westeurope"),
}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "newResourceGroup@eastus2")
assert.DeepEqual(t, data, aciContext("1234", "newResourceGroup", "eastus2"))
}
func TestSelectExistingResourceGroup(t *testing.T) {
ctx := context.TODO()
opts := options("1234", "")
selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("1234", "Subscription1")}, nil)
m.userPrompt.On("Select", "Select a resource group", selectOptions).Return(2, nil)
m.resourceGroupHelper.On("ListGroups", ctx, "1234").Return([]resources.Group{
group("group1", "eastus"),
group("group2", "westeurope"),
}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "group2@westeurope")
assert.DeepEqual(t, data, aciContext("1234", "group2", "westeurope"))
}
func TestSelectSingleSubscriptionIdAndExistingResourceGroup(t *testing.T) {
ctx := context.TODO()
opts := options("", "")
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("123456", "Subscription1")}, nil)
selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
m.userPrompt.On("Select", "Select a resource group", selectOptions).Return(2, nil)
m.resourceGroupHelper.On("ListGroups", ctx, "123456").Return([]resources.Group{
group("group1", "eastus"),
group("group2", "westeurope"),
}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "group2@westeurope")
assert.DeepEqual(t, data, aciContext("123456", "group2", "westeurope"))
}
func TestSelectSubscriptionIdAndExistingResourceGroup(t *testing.T) {
ctx := context.TODO()
opts := options("", "")
sub1 := subModel("1234", "Subscription1")
sub2 := subModel("5678", "Subscription2")
m := testContextMocks()
m.resourceGroupHelper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{sub1, sub2}, nil)
selectOptions := []string{"Subscription1 (1234)", "Subscription2 (5678)"}
m.userPrompt.On("Select", "Select a subscription ID", selectOptions).Return(1, nil)
selectOptions = []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
m.userPrompt.On("Select", "Select a resource group", selectOptions).Return(2, nil)
m.resourceGroupHelper.On("ListGroups", ctx, "5678").Return([]resources.Group{
group("group1", "eastus"),
group("group2", "westeurope"),
}, nil)
data, description, err := m.contextCreateHelper.createContextData(ctx, opts)
assert.NilError(t, err)
assert.Equal(t, description, "group2@westeurope")
assert.DeepEqual(t, data, aciContext("5678", "group2", "westeurope"))
}
func subModel(subID string, display string) subscription.Model {
return subscription.Model{
SubscriptionID: to.StringPtr(subID),
DisplayName: to.StringPtr(display),
}
}
func group(groupName string, location string) resources.Group {
return resources.Group{
Name: to.StringPtr(groupName),
Location: to.StringPtr(location),
}
}
func aciContext(subscriptionID string, resourceGroupName string, location string) store.AciContext {
return store.AciContext{
SubscriptionID: subscriptionID,
Location: location,
ResourceGroup: resourceGroupName,
}
}
func options(subscriptionID string, resourceGroupName string) ContextParams {
return ContextParams{
SubscriptionID: subscriptionID,
ResourceGroup: resourceGroupName,
}
}
type mockUserPrompt struct {
mock.Mock
}
func (s *mockUserPrompt) Select(message string, options []string) (int, error) {
args := s.Called(message, options)
return args.Int(0), args.Error(1)
}
func (s *mockUserPrompt) Confirm(message string, defaultValue bool) (bool, error) {
args := s.Called(message, options)
return args.Bool(0), args.Error(1)
}
func (s *mockUserPrompt) Input(message string, defaultValue string) (string, error) {
args := s.Called(message, options)
return args.String(0), args.Error(1)
}
func (s *mockUserPrompt) Password(message string) (string, error) {
args := s.Called(message, options)
return args.String(0), args.Error(1)
}
type MockResourceGroupHelper struct {
mock.Mock
}
func (s *MockResourceGroupHelper) GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error) {
args := s.Called(ctx)
return args.Get(0).([]subscription.Model), args.Error(1)
}
func (s *MockResourceGroupHelper) ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error) {
args := s.Called(ctx, subscriptionID)
return args.Get(0).([]resources.Group), args.Error(1)
}
func (s *MockResourceGroupHelper) GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error) {
args := s.Called(ctx, subscriptionID, groupName)
return args.Get(0).(resources.Group), args.Error(1)
}
func (s *MockResourceGroupHelper) CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error) {
args := s.Called(ctx, subscriptionID, resourceGroupName, parameters)
return args.Get(0).(resources.Group), args.Error(1)
}
func (s *MockResourceGroupHelper) DeleteAsync(ctx context.Context, subscriptionID string, resourceGroupName string) (err error) {
args := s.Called(ctx, subscriptionID, resourceGroupName)
return args.Error(0)
}

查看文件

@ -1,94 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"fmt"
"strings"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose-cli/api/containers"
)
// ContainerToComposeProject convert container config to compose project
func ContainerToComposeProject(r containers.ContainerConfig) (types.Project, error) {
var ports []types.ServicePortConfig
for _, p := range r.Ports {
ports = append(ports, types.ServicePortConfig{
Target: p.ContainerPort,
Published: p.HostPort,
Protocol: p.Protocol,
})
}
projectVolumes, serviceConfigVolumes, err := GetRunVolumes(r.Volumes)
if err != nil {
return types.Project{}, err
}
retries := uint64(r.Healthcheck.Retries)
project := types.Project{
Name: r.ID,
Services: []types.ServiceConfig{
{
Name: r.ID,
Image: r.Image,
Command: r.Command,
Ports: ports,
Labels: r.Labels,
Volumes: serviceConfigVolumes,
DomainName: r.DomainName,
Environment: toComposeEnvs(r.Environment),
HealthCheck: &types.HealthCheckConfig{
Test: r.Healthcheck.Test,
Timeout: &r.Healthcheck.Timeout,
Interval: &r.Healthcheck.Interval,
Retries: &retries,
StartPeriod: &r.Healthcheck.StartPeriod,
Disable: r.Healthcheck.Disable,
},
Deploy: &types.DeployConfig{
Resources: types.Resources{
Reservations: &types.Resource{
NanoCPUs: fmt.Sprintf("%f", r.CPULimit),
MemoryBytes: types.UnitBytes(r.MemLimit.Value()),
},
},
RestartPolicy: &types.RestartPolicy{
Condition: r.RestartPolicyCondition,
},
},
},
},
Volumes: projectVolumes,
}
return project, nil
}
func toComposeEnvs(opts []string) types.MappingWithEquals {
result := types.MappingWithEquals{}
for _, env := range opts {
tokens := strings.SplitN(env, "=", 2)
if len(tokens) > 1 {
result[tokens[0]] = &tokens[1]
} else {
result[env] = nil
}
}
return result
}

查看文件

@ -1,87 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"testing"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"gotest.tools/v3/assert"
"github.com/docker/compose-cli/api/containers"
)
func TestConvertContainerEnvironment(t *testing.T) {
container := containers.ContainerConfig{
ID: "container1",
Command: []string{"echo", "Hello!"},
Environment: []string{"key1=value1", "key2", "key3=value3"},
}
project, err := ContainerToComposeProject(container)
assert.NilError(t, err)
service1 := project.Services[0]
assert.Equal(t, service1.Name, container.ID)
assert.DeepEqual(t, []string(service1.Command), container.Command)
assert.DeepEqual(t, service1.Environment, types.MappingWithEquals{
"key1": to.StringPtr("value1"),
"key2": nil,
"key3": to.StringPtr("value3"),
})
}
func TestConvertRestartPolicy(t *testing.T) {
container := containers.ContainerConfig{
ID: "container1",
RestartPolicyCondition: "none",
}
project, err := ContainerToComposeProject(container)
assert.NilError(t, err)
service1 := project.Services[0]
assert.Equal(t, service1.Name, container.ID)
assert.Equal(t, service1.Deploy.RestartPolicy.Condition, "none")
}
func TestConvertDomainName(t *testing.T) {
container := containers.ContainerConfig{
ID: "container1",
DomainName: "myapp",
}
project, err := ContainerToComposeProject(container)
assert.NilError(t, err)
service1 := project.Services[0]
assert.Equal(t, service1.Name, container.ID)
assert.Equal(t, service1.DomainName, "myapp")
}
func TestConvertEnvVariables(t *testing.T) {
container := containers.ContainerConfig{
ID: "container1",
Environment: []string{
"key=value",
"key2=value=with=equal",
},
}
project, err := ContainerToComposeProject(container)
assert.NilError(t, err)
service1 := project.Services[0]
assert.Equal(t, service1.Name, container.ID)
assert.DeepEqual(t, service1.Environment, types.MappingWithEquals{
"key": to.StringPtr("value"),
"key2": to.StringPtr("value=with=equal"),
})
}

查看文件

@ -1,456 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/utils/formatter"
)
const (
// StatusRunning name of the ACI running status
StatusRunning = "Running"
// ComposeDNSSidecarName name of the dns sidecar container
ComposeDNSSidecarName = "aci--dns--sidecar"
dnsSidecarImage = "docker/aci-hostnames-sidecar:1.0"
)
// ToContainerGroup converts a compose project into a ACI container group
func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project, storageHelper login.StorageLogin) (containerinstance.ContainerGroup, error) {
project := projectAciHelper(p)
containerGroupName := strings.ToLower(project.Name)
volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
if err != nil {
return containerinstance.ContainerGroup{}, err
}
secretVolumes, err := project.getAciSecretVolumes()
if err != nil {
return containerinstance.ContainerGroup{}, err
}
allVolumes := append(volumesSlice, secretVolumes...)
var volumes *[]containerinstance.Volume
if len(allVolumes) > 0 {
volumes = &allVolumes
}
registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader())
if err != nil {
return containerinstance.ContainerGroup{}, err
}
var ctnrs []containerinstance.Container
restartPolicy, err := project.getRestartPolicy()
if err != nil {
return containerinstance.ContainerGroup{}, err
}
groupDefinition := containerinstance.ContainerGroup{
Name: &containerGroupName,
Location: &aciContext.Location,
ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
OsType: containerinstance.Linux,
Containers: &ctnrs,
Volumes: volumes,
ImageRegistryCredentials: &registryCreds,
RestartPolicy: restartPolicy,
},
}
var groupPorts []containerinstance.Port
var dnsLabelName *string
for _, s := range project.Services {
service := serviceConfigAciHelper(s)
containerDefinition, err := service.getAciContainer()
if err != nil {
return containerinstance.ContainerGroup{}, err
}
if service.Labels != nil && len(service.Labels) > 0 {
return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications")
}
containerPorts, serviceGroupPorts, serviceDomainName, err := convertPortsToAci(service)
if err != nil {
return groupDefinition, err
}
containerDefinition.ContainerProperties.Ports = &containerPorts
groupPorts = append(groupPorts, serviceGroupPorts...)
if serviceDomainName != nil {
if dnsLabelName != nil && *serviceDomainName != *dnsLabelName {
return containerinstance.ContainerGroup{}, fmt.Errorf("ACI integration does not support specifying different domain names on services in the same compose application")
}
dnsLabelName = serviceDomainName
}
ctnrs = append(ctnrs, containerDefinition)
}
if len(groupPorts) > 0 {
groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
Type: containerinstance.Public,
Ports: &groupPorts,
DNSNameLabel: dnsLabelName,
}
}
if len(project.Services) > 1 {
dnsSideCar := getDNSSidecar(project.Services)
ctnrs = append(ctnrs, dnsSideCar)
}
groupDefinition.ContainerGroupProperties.Containers = &ctnrs
return groupDefinition, nil
}
func durationToSeconds(d *types.Duration) *int32 {
if d == nil || *d == 0 {
return nil
}
v := int32(time.Duration(*d).Seconds())
return &v
}
func getDNSSidecar(services types.Services) containerinstance.Container {
names := []string{"/hosts"}
for _, service := range services {
names = append(names, service.Name)
if service.ContainerName != "" {
names = append(names, service.ContainerName)
}
}
dnsSideCar := containerinstance.Container{
Name: to.StringPtr(ComposeDNSSidecarName),
ContainerProperties: &containerinstance.ContainerProperties{
Image: to.StringPtr(dnsSidecarImage),
Command: &names,
Resources: &containerinstance.ResourceRequirements{
Requests: &containerinstance.ResourceRequests{
MemoryInGB: to.Float64Ptr(0.1),
CPU: to.Float64Ptr(0.01),
},
},
},
}
return dnsSideCar
}
type projectAciHelper types.Project
type serviceConfigAciHelper types.ServiceConfig
func (s serviceConfigAciHelper) getAciContainer() (containerinstance.Container, error) {
aciServiceVolumes, err := s.getAciFileVolumeMounts()
if err != nil {
return containerinstance.Container{}, err
}
serviceSecretVolumes, err := s.getAciSecretsVolumeMounts()
if err != nil {
return containerinstance.Container{}, err
}
allVolumes := append(aciServiceVolumes, serviceSecretVolumes...)
var volumes *[]containerinstance.VolumeMount
if len(allVolumes) > 0 {
volumes = &allVolumes
}
resource, err := s.getResourceRequestsLimits()
if err != nil {
return containerinstance.Container{}, err
}
containerName := s.Name
if s.ContainerName != "" {
containerName = s.ContainerName
}
return containerinstance.Container{
Name: to.StringPtr(containerName),
ContainerProperties: &containerinstance.ContainerProperties{
Image: to.StringPtr(s.Image),
Command: to.StringSlicePtr(s.Command),
EnvironmentVariables: getEnvVariables(s.Environment),
Resources: resource,
VolumeMounts: volumes,
LivenessProbe: s.getLivenessProbe(),
},
}, nil
}
func (s serviceConfigAciHelper) getResourceRequestsLimits() (*containerinstance.ResourceRequirements, error) {
memRequest := 1. // Default 1 Gb
var cpuRequest float64 = 1
var err error
hasMemoryRequest := func() bool {
return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != 0
}
hasCPURequest := func() bool {
return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.NanoCPUs != ""
}
if hasMemoryRequest() {
memRequest = BytesToGB(float64(s.Deploy.Resources.Reservations.MemoryBytes))
}
if hasCPURequest() {
cpuRequest, err = strconv.ParseFloat(s.Deploy.Resources.Reservations.NanoCPUs, 0)
if err != nil {
return nil, err
}
}
memLimit := memRequest
cpuLimit := cpuRequest
if s.Deploy != nil && s.Deploy.Resources.Limits != nil {
if s.Deploy.Resources.Limits.MemoryBytes != 0 {
memLimit = BytesToGB(float64(s.Deploy.Resources.Limits.MemoryBytes))
if !hasMemoryRequest() {
memRequest = memLimit
}
}
if s.Deploy.Resources.Limits.NanoCPUs != "" {
cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0)
if err != nil {
return nil, err
}
if !hasCPURequest() {
cpuRequest = cpuLimit
}
}
}
resources := containerinstance.ResourceRequirements{
Requests: &containerinstance.ResourceRequests{
MemoryInGB: to.Float64Ptr(memRequest),
CPU: to.Float64Ptr(cpuRequest),
},
Limits: &containerinstance.ResourceLimits{
MemoryInGB: to.Float64Ptr(memLimit),
CPU: to.Float64Ptr(cpuLimit),
},
}
return &resources, nil
}
func (s serviceConfigAciHelper) getLivenessProbe() *containerinstance.ContainerProbe {
if s.HealthCheck != nil && !s.HealthCheck.Disable && len(s.HealthCheck.Test) > 0 {
testArray := s.HealthCheck.Test
switch s.HealthCheck.Test[0] {
case "NONE", "CMD", "CMD-SHELL":
testArray = s.HealthCheck.Test[1:]
}
if len(testArray) == 0 {
return nil
}
var retries *int32
if s.HealthCheck.Retries != nil {
retries = to.Int32Ptr(int32(*s.HealthCheck.Retries))
}
probe := containerinstance.ContainerProbe{
Exec: &containerinstance.ContainerExec{
Command: to.StringSlicePtr(testArray),
},
InitialDelaySeconds: durationToSeconds(s.HealthCheck.StartPeriod),
PeriodSeconds: durationToSeconds(s.HealthCheck.Interval),
TimeoutSeconds: durationToSeconds(s.HealthCheck.Timeout),
}
if retries != nil && *retries > 0 {
probe.FailureThreshold = retries
}
return &probe
}
return nil
}
func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable {
result := []containerinstance.EnvironmentVariable{}
for key, value := range composeEnv {
var strValue string
if value == nil {
strValue = os.Getenv(key)
} else {
strValue = *value
}
result = append(result, containerinstance.EnvironmentVariable{
Name: to.StringPtr(key),
Value: to.StringPtr(strValue),
})
}
return &result
}
// BytesToGB convert bytes To GB
func BytesToGB(b float64) float64 {
f := b / 1024 / 1024 / 1024 // from bytes to gigabytes
return math.Round(f*100) / 100
}
func gbToBytes(memInBytes float64) uint64 {
return uint64(memInBytes * 1024 * 1024 * 1024)
}
// ContainerGroupToServiceStatus convert from an ACI container definition to service status
func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) api.ServiceStatus {
var replicas = 1
if GetStatus(container, group) != StatusRunning {
replicas = 0
}
return api.ServiceStatus{
ID: containerID,
Name: *container.Name,
Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), FQDN(group, region)),
Replicas: replicas,
Desired: 1,
}
}
// FQDN retrieve the fully qualified domain name for a ContainerGroup
func FQDN(group containerinstance.ContainerGroup, region string) string {
fqdn := ""
if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
}
return fqdn
}
// ContainerGroupToContainer composes a Container from an ACI container definition
func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
command := ""
if cc.Command != nil {
command = strings.Join(*cc.Command, " ")
}
status := GetStatus(cc, cg)
platform := string(cg.OsType)
var envVars map[string]string
if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 {
envVars = map[string]string{}
for _, envVar := range *cc.EnvironmentVariables {
envVars[*envVar.Name] = *envVar.Value
}
}
hostConfig := ToHostConfig(cc, cg)
config := &containers.RuntimeConfig{
FQDN: FQDN(cg, region),
Env: envVars,
}
var healthcheck = containers.Healthcheck{
Disable: true,
}
if cc.LivenessProbe != nil &&
cc.LivenessProbe.Exec != nil &&
cc.LivenessProbe.Exec.Command != nil {
if len(*cc.LivenessProbe.Exec.Command) > 0 {
healthcheck.Disable = false
healthcheck.Test = *cc.LivenessProbe.Exec.Command
if cc.LivenessProbe.PeriodSeconds != nil {
healthcheck.Interval = types.Duration(int64(*cc.LivenessProbe.PeriodSeconds) * int64(time.Second))
}
if cc.LivenessProbe.FailureThreshold != nil {
healthcheck.Retries = int(*cc.LivenessProbe.FailureThreshold)
}
if cc.LivenessProbe.TimeoutSeconds != nil {
healthcheck.Timeout = types.Duration(int64(*cc.LivenessProbe.TimeoutSeconds) * int64(time.Second))
}
if cc.LivenessProbe.InitialDelaySeconds != nil {
healthcheck.StartPeriod = types.Duration(int64(*cc.LivenessProbe.InitialDelaySeconds) * int64(time.Second))
}
}
}
c := containers.Container{
ID: containerID,
Status: status,
Image: to.String(cc.Image),
Command: command,
CPUTime: 0,
MemoryUsage: 0,
PidsCurrent: 0,
PidsLimit: 0,
Ports: ToPorts(cg.IPAddress, *cc.Ports),
Platform: platform,
Config: config,
HostConfig: hostConfig,
Healthcheck: healthcheck,
}
return c
}
// ToHostConfig convert an ACI container to host config value
func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig {
memLimits := uint64(0)
memRequest := uint64(0)
cpuLimit := 0.
cpuReservation := 0.
if cc.Resources != nil {
if cc.Resources.Limits != nil {
if cc.Resources.Limits.MemoryInGB != nil {
memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB)
}
if cc.Resources.Limits.CPU != nil {
cpuLimit = *cc.Resources.Limits.CPU
}
}
if cc.Resources.Requests != nil {
if cc.Resources.Requests.MemoryInGB != nil {
memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB)
}
if cc.Resources.Requests.CPU != nil {
cpuReservation = *cc.Resources.Requests.CPU
}
}
}
hostConfig := &containers.HostConfig{
CPULimit: cpuLimit,
CPUReservation: cpuReservation,
MemoryLimit: memLimits,
MemoryReservation: memRequest,
RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy),
}
return hostConfig
}
// GetStatus returns status for the specified container
func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
status := GetGroupStatus(group)
if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
status = *container.InstanceView.CurrentState.State
}
return status
}
// GetGroupStatus returns status for the container group
func GetGroupStatus(group containerinstance.ContainerGroup) string {
if group.InstanceView != nil && group.InstanceView.State != nil {
return "Node " + *group.InstanceView.State
}
return api.UNKNOWN
}

查看文件

@ -1,600 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"os"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/pkg/api"
)
var (
convertCtx = store.AciContext{
SubscriptionID: "subID",
ResourceGroup: "rg",
Location: "eu",
}
mockStorageHelper = &mockStorageLogin{}
)
func TestProjectName(t *testing.T) {
project := types.Project{
Name: "TEST",
}
containerGroup, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Equal(t, *containerGroup.Name, "test")
}
func TestContainerGroupToContainer(t *testing.T) {
myContainerGroup := containerinstance.ContainerGroup{
ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
IPAddress: &containerinstance.IPAddress{
Ports: &[]containerinstance.Port{{
Port: to.Int32Ptr(80),
}},
IP: to.StringPtr("42.42.42.42"),
DNSNameLabel: to.StringPtr("myapp"),
},
OsType: "Linux",
},
}
myContainer := containerinstance.Container{
Name: to.StringPtr("myContainerID"),
ContainerProperties: &containerinstance.ContainerProperties{
Image: to.StringPtr("sha256:666"),
Command: to.StringSlicePtr([]string{"mycommand"}),
Ports: &[]containerinstance.ContainerPort{{
Port: to.Int32Ptr(80),
}},
EnvironmentVariables: nil,
InstanceView: &containerinstance.ContainerPropertiesInstanceView{
RestartCount: nil,
CurrentState: &containerinstance.ContainerState{
State: to.StringPtr("Running"),
},
},
Resources: &containerinstance.ResourceRequirements{
Limits: &containerinstance.ResourceLimits{
CPU: to.Float64Ptr(3),
MemoryInGB: to.Float64Ptr(0.2),
},
Requests: &containerinstance.ResourceRequests{
CPU: to.Float64Ptr(2),
MemoryInGB: to.Float64Ptr(0.1),
},
},
LivenessProbe: &containerinstance.ContainerProbe{
Exec: &containerinstance.ContainerExec{
Command: to.StringSlicePtr([]string{
"my",
"command",
"--option",
}),
},
PeriodSeconds: to.Int32Ptr(10),
FailureThreshold: to.Int32Ptr(3),
InitialDelaySeconds: to.Int32Ptr(2),
TimeoutSeconds: to.Int32Ptr(1),
},
},
}
var expectedContainer = containers.Container{
ID: "myContainerID",
Status: "Running",
Image: "sha256:666",
Command: "mycommand",
Platform: "Linux",
Ports: []containers.Port{{
HostPort: uint32(80),
ContainerPort: uint32(80),
Protocol: "tcp",
HostIP: "42.42.42.42",
}},
Config: &containers.RuntimeConfig{
FQDN: "myapp.eastus.azurecontainer.io",
},
HostConfig: &containers.HostConfig{
CPULimit: 3,
CPUReservation: 2,
MemoryLimit: gbToBytes(0.2),
MemoryReservation: gbToBytes(0.1),
RestartPolicy: "any",
},
Healthcheck: containers.Healthcheck{
Disable: false,
Test: []string{
"my",
"command",
"--option",
},
Interval: types.Duration(10 * time.Second),
Retries: 3,
StartPeriod: types.Duration(2 * time.Second),
Timeout: types.Duration(time.Second),
},
}
container := ContainerGroupToContainer("myContainerID", myContainerGroup, myContainer, "eastus")
assert.DeepEqual(t, container, expectedContainer)
}
func TestHealthcheckTranslation(t *testing.T) {
test := []string{
"my",
"command",
"--option",
}
interval := types.Duration(10 * time.Second)
retries := uint64(42)
startPeriod := types.Duration(2 * time.Second)
timeout := types.Duration(3 * time.Second)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
HealthCheck: &types.HealthCheckConfig{
Test: test,
Timeout: &timeout,
Interval: &interval,
Retries: &retries,
StartPeriod: &startPeriod,
Disable: false,
},
},
},
}
testHealthcheckTestPrefixRemoval := func(test []string, shellPreffix ...string) {
project.Services[0].HealthCheck.Test = append(shellPreffix, test...)
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.DeepEqual(t, (*group.Containers)[0].LivenessProbe.Exec.Command, to.StringSlicePtr(test))
assert.Equal(t, *(*group.Containers)[0].LivenessProbe.PeriodSeconds, int32(10))
assert.Assert(t, (*group.Containers)[0].LivenessProbe.SuccessThreshold == nil)
assert.Equal(t, *(*group.Containers)[0].LivenessProbe.FailureThreshold, int32(42))
assert.Equal(t, *(*group.Containers)[0].LivenessProbe.InitialDelaySeconds, int32(2))
assert.Equal(t, *(*group.Containers)[0].LivenessProbe.TimeoutSeconds, int32(3))
}
testHealthcheckTestPrefixRemoval(test)
testHealthcheckTestPrefixRemoval(test, "NONE")
testHealthcheckTestPrefixRemoval(test, "CMD")
testHealthcheckTestPrefixRemoval(test, "CMD-SHELL")
project.Services[0].HealthCheck.Disable = true
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, (*group.Containers)[0].LivenessProbe == nil)
}
func TestHealthcheckTranslationZeroValues(t *testing.T) {
test := []string{
"my",
"command",
"--option",
}
interval := types.Duration(0)
retries := uint64(0)
startPeriod := types.Duration(0)
timeout := types.Duration(0)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
HealthCheck: &types.HealthCheckConfig{
Test: test,
Timeout: &timeout,
Interval: &interval,
Retries: &retries,
StartPeriod: &startPeriod,
Disable: false,
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.DeepEqual(t, (*group.Containers)[0].LivenessProbe.Exec.Command, to.StringSlicePtr(test))
assert.Assert(t, (*group.Containers)[0].LivenessProbe.PeriodSeconds == nil)
assert.Assert(t, (*group.Containers)[0].LivenessProbe.SuccessThreshold == nil)
assert.Assert(t, (*group.Containers)[0].LivenessProbe.FailureThreshold == nil)
assert.Assert(t, (*group.Containers)[0].LivenessProbe.InitialDelaySeconds == nil)
assert.Assert(t, (*group.Containers)[0].LivenessProbe.TimeoutSeconds == nil)
}
func TestContainerGroupToServiceStatus(t *testing.T) {
myContainerGroup := containerinstance.ContainerGroup{
ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
IPAddress: &containerinstance.IPAddress{
Ports: &[]containerinstance.Port{{
Port: to.Int32Ptr(80),
}},
IP: to.StringPtr("42.42.42.42"),
},
},
}
myContainer := containerinstance.Container{
Name: to.StringPtr("myContainerID"),
ContainerProperties: &containerinstance.ContainerProperties{
Ports: &[]containerinstance.ContainerPort{{
Port: to.Int32Ptr(80),
}},
InstanceView: &containerinstance.ContainerPropertiesInstanceView{
RestartCount: nil,
CurrentState: &containerinstance.ContainerState{
State: to.StringPtr("Running"),
},
},
},
}
var expectedService = api.ServiceStatus{
ID: "myContainerID",
Name: "myContainerID",
Ports: []string{"42.42.42.42:80->80/tcp"},
Replicas: 1,
Desired: 1,
}
container := ContainerGroupToServiceStatus("myContainerID", myContainerGroup, myContainer, "eastus")
assert.DeepEqual(t, container, expectedService)
}
func TestComposeContainerGroupToContainerWithDnsSideCarSide(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
},
{
Name: "service2",
Image: "image2",
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
assert.Equal(t, *(*group.Containers)[1].Name, "service2")
assert.Equal(t, *(*group.Containers)[2].Name, ComposeDNSSidecarName)
assert.DeepEqual(t, *(*group.Containers)[2].Command, []string{"/hosts", "service1", "service2"})
assert.Equal(t, *(*group.Containers)[0].Image, "image1")
assert.Equal(t, *(*group.Containers)[1].Image, "image2")
assert.Equal(t, *(*group.Containers)[2].Image, dnsSidecarImage)
}
func TestComposeSingleContainerGroupToContainerNoDnsSideCarSide(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 1))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
assert.Equal(t, *(*group.Containers)[0].Image, "image1")
}
func TestLabelsErrorMessage(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Labels: map[string]string{
"label1": "value1",
},
},
},
}
_, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.Error(t, err, "ACI integration does not support labels in compose applications")
}
func TestComposeContainerGroupToContainerWithDomainName(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Ports: []types.ServicePortConfig{
{
Published: 80,
Target: 80,
},
},
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
Ports: []types.ServicePortConfig{
{
Published: 8080,
Target: 8080,
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
groupPorts := *group.IPAddress.Ports
assert.Assert(t, is.Len(groupPorts, 2))
assert.Equal(t, *groupPorts[0].Port, int32(80))
assert.Equal(t, *groupPorts[1].Port, int32(8080))
assert.Equal(t, *group.IPAddress.DNSNameLabel, "myApp")
}
func TestComposeContainerGroupToContainerErrorWhenSeveralDomainNames(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
DomainName: "myApp2",
},
},
}
_, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.Error(t, err, "ACI integration does not support specifying different domain names on services in the same compose application")
}
// ACI fails if group definition IPAddress has no ports
func TestComposeContainerGroupToContainerIgnoreDomainNameWithoutPorts(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
DomainName: "myApp",
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
assert.Assert(t, group.IPAddress == nil)
}
var _0_1Gb = gbToBytes(0.1)
func TestComposeContainerGroupToContainerResourceRequests(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
Resources: types.Resources{
Reservations: &types.Resource{
NanoCPUs: "0.1",
MemoryBytes: types.UnitBytes(_0_1Gb),
},
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
request := *((*group.Containers)[0]).Resources.Requests
assert.Equal(t, *request.CPU, float64(0.1))
assert.Equal(t, *request.MemoryInGB, float64(0.1))
limits := *((*group.Containers)[0]).Resources.Limits
assert.Equal(t, *limits.CPU, float64(0.1))
assert.Equal(t, *limits.MemoryInGB, float64(0.1))
}
func TestComposeContainerGroupToContainerResourceRequestsAndLimits(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
Resources: types.Resources{
Reservations: &types.Resource{
NanoCPUs: "0.1",
MemoryBytes: types.UnitBytes(_0_1Gb),
},
Limits: &types.Resource{
NanoCPUs: "0.3",
MemoryBytes: types.UnitBytes(2 * _0_1Gb),
},
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
request := *((*group.Containers)[0]).Resources.Requests
assert.Equal(t, *request.CPU, float64(0.1))
assert.Equal(t, *request.MemoryInGB, float64(0.1))
limits := *((*group.Containers)[0]).Resources.Limits
assert.Equal(t, *limits.CPU, float64(0.3))
assert.Equal(t, *limits.MemoryInGB, float64(0.2))
}
func TestComposeContainerGroupToContainerResourceLimitsOnly(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
Resources: types.Resources{
Limits: &types.Resource{
NanoCPUs: "0.3",
MemoryBytes: types.UnitBytes(2 * _0_1Gb),
},
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
request := *((*group.Containers)[0]).Resources.Requests
assert.Equal(t, *request.CPU, float64(0.3))
assert.Equal(t, *request.MemoryInGB, float64(0.2))
limits := *((*group.Containers)[0]).Resources.Limits
assert.Equal(t, *limits.CPU, float64(0.3))
assert.Equal(t, *limits.MemoryInGB, float64(0.2))
}
func TestComposeContainerGroupToContainerResourceRequestsDefaults(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
Resources: types.Resources{
Reservations: &types.Resource{
NanoCPUs: "",
MemoryBytes: 0,
},
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
request := *((*group.Containers)[0]).Resources.Requests
assert.Equal(t, *request.CPU, float64(1))
assert.Equal(t, *request.MemoryInGB, float64(1))
}
func TestComposeContainerGroupToContainerenvVar(t *testing.T) {
err := os.Setenv("key2", "value2")
assert.NilError(t, err)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Environment: types.MappingWithEquals{
"key1": to.StringPtr("value1"),
"key2": nil,
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
envVars := *((*group.Containers)[0]).EnvironmentVariables
assert.Assert(t, is.Len(envVars, 2))
assert.Assert(t, is.Contains(envVars, containerinstance.EnvironmentVariable{Name: to.StringPtr("key1"), Value: to.StringPtr("value1")}))
assert.Assert(t, is.Contains(envVars, containerinstance.EnvironmentVariable{Name: to.StringPtr("key2"), Value: to.StringPtr("value2")}))
}
func TestConvertContainerGroupStatus(t *testing.T) {
assert.Equal(t, "Running", GetStatus(container(to.StringPtr("Running")), group(to.StringPtr("Started"))))
assert.Equal(t, "Terminated", GetStatus(container(to.StringPtr("Terminated")), group(to.StringPtr("Stopped"))))
assert.Equal(t, "Node Stopped", GetStatus(container(nil), group(to.StringPtr("Stopped"))))
assert.Equal(t, "Node Started", GetStatus(container(nil), group(to.StringPtr("Started"))))
assert.Equal(t, "Running", GetStatus(container(to.StringPtr("Running")), group(nil)))
assert.Equal(t, "Unknown", GetStatus(container(nil), group(nil)))
}
func container(status *string) containerinstance.Container {
var state *containerinstance.ContainerState
if status != nil {
state = &containerinstance.ContainerState{
State: status,
}
}
return containerinstance.Container{
ContainerProperties: &containerinstance.ContainerProperties{
InstanceView: &containerinstance.ContainerPropertiesInstanceView{
CurrentState: state,
},
},
}
}
func group(status *string) containerinstance.ContainerGroup {
var view *containerinstance.ContainerGroupPropertiesInstanceView
if status != nil {
view = &containerinstance.ContainerGroupPropertiesInstanceView{
State: status,
}
}
return containerinstance.ContainerGroup{
ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
InstanceView: view,
},
}
}

查看文件

@ -1,94 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
"github.com/docker/compose-cli/api/containers"
)
func convertPortsToAci(service serviceConfigAciHelper) ([]containerinstance.ContainerPort, []containerinstance.Port, *string, error) {
var groupPorts []containerinstance.Port
var containerPorts []containerinstance.ContainerPort
for _, portConfig := range service.Ports {
if portConfig.Published != 0 && portConfig.Published != portConfig.Target {
msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s",
portConfig.Published, portConfig.Target, service.Name)
return nil, nil, nil, errors.New(msg)
}
portNumber := int32(portConfig.Target)
var groupProtocol containerinstance.ContainerGroupNetworkProtocol
var containerProtocol containerinstance.ContainerNetworkProtocol
switch portConfig.Protocol {
case "tcp", "":
groupProtocol = containerinstance.TCP
containerProtocol = containerinstance.ContainerNetworkProtocolTCP
case "udp":
groupProtocol = containerinstance.UDP
containerProtocol = containerinstance.ContainerNetworkProtocolUDP
default:
return nil, nil, nil, fmt.Errorf("unknown protocol %q in exposed port for service %q", portConfig.Protocol, service.Name)
}
containerPorts = append(containerPorts, containerinstance.ContainerPort{
Port: to.Int32Ptr(portNumber),
Protocol: containerProtocol,
})
groupPorts = append(groupPorts, containerinstance.Port{
Port: to.Int32Ptr(portNumber),
Protocol: groupProtocol,
})
}
var dnsLabelName *string
if service.DomainName != "" {
dnsLabelName = &service.DomainName
}
return containerPorts, groupPorts, dnsLabelName, nil
}
// ToPorts converts Azure container ports to api ports
func ToPorts(ipAddr *containerinstance.IPAddress, ports []containerinstance.ContainerPort) []containers.Port {
var result []containers.Port
for _, port := range ports {
if port.Port == nil {
continue
}
protocol := "tcp"
if port.Protocol != "" {
protocol = string(port.Protocol)
}
ip := ""
if ipAddr != nil && ipAddr.IP != nil {
ip = *ipAddr.IP
}
result = append(result, containers.Port{
HostPort: uint32(*port.Port),
ContainerPort: uint32(*port.Port),
HostIP: ip,
Protocol: strings.ToLower(protocol),
})
}
return result
}

查看文件

@ -1,257 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"github.com/docker/compose-cli/api/containers"
)
func TestComposeContainerGroupToContainerMultiplePorts(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Ports: []types.ServicePortConfig{
{
Published: 80,
Target: 80,
},
},
},
{
Name: "service2",
Image: "image2",
Ports: []types.ServicePortConfig{
{
Published: 8080,
Target: 8080,
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
container1 := (*group.Containers)[0]
assert.Equal(t, *container1.Name, "service1")
assert.Equal(t, *container1.Image, "image1")
assert.Equal(t, *(*container1.Ports)[0].Port, int32(80))
container2 := (*group.Containers)[1]
assert.Equal(t, *container2.Name, "service2")
assert.Equal(t, *container2.Image, "image2")
assert.Equal(t, *(*container2.Ports)[0].Port, int32(8080))
groupPorts := *group.IPAddress.Ports
assert.Assert(t, is.Len(groupPorts, 2))
assert.Equal(t, *groupPorts[0].Port, int32(80))
assert.Equal(t, *groupPorts[1].Port, int32(8080))
assert.Assert(t, group.IPAddress.DNSNameLabel == nil)
}
func TestPortConvert(t *testing.T) {
expectedPorts := []containers.Port{
{
HostPort: 80,
ContainerPort: 80,
HostIP: "10.0.0.1",
Protocol: "tcp",
},
}
testCases := []struct {
name string
ip *containerinstance.IPAddress
ports []containerinstance.ContainerPort
expected []containers.Port
}{
{
name: "convert port",
ip: &containerinstance.IPAddress{
IP: to.StringPtr("10.0.0.1"),
},
ports: []containerinstance.ContainerPort{
{
Protocol: "tcp",
Port: to.Int32Ptr(80),
},
},
expected: expectedPorts,
},
{
name: "with nil ip",
ip: nil,
ports: []containerinstance.ContainerPort{
{
Protocol: "tcp",
Port: to.Int32Ptr(80),
},
},
expected: []containers.Port{
{
HostPort: 80,
ContainerPort: 80,
HostIP: "",
Protocol: "tcp",
},
},
},
{
name: "with nil ip value",
ip: &containerinstance.IPAddress{
IP: nil,
},
ports: []containerinstance.ContainerPort{
{
Protocol: "tcp",
Port: to.Int32Ptr(80),
},
},
expected: []containers.Port{
{
HostPort: 80,
ContainerPort: 80,
HostIP: "",
Protocol: "tcp",
},
},
},
{
name: "skip nil ports",
ip: nil,
ports: []containerinstance.ContainerPort{
{
Protocol: "tcp",
Port: to.Int32Ptr(80),
},
{
Protocol: "tcp",
Port: nil,
},
},
expected: []containers.Port{
{
HostPort: 80,
ContainerPort: 80,
HostIP: "",
Protocol: "tcp",
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ports := ToPorts(testCase.ip, testCase.ports)
assert.DeepEqual(t, testCase.expected, ports)
})
}
}
func TestConvertTCPPortsToAci(t *testing.T) {
service := types.ServiceConfig{
Name: "myService",
Ports: []types.ServicePortConfig{
{
Protocol: "",
Target: 80,
Published: 80,
},
{
Protocol: "tcp",
Target: 90,
Published: 90,
},
},
}
containerPorts, groupPports, _, err := convertPortsToAci(serviceConfigAciHelper(service))
assert.NilError(t, err)
assert.DeepEqual(t, containerPorts, []containerinstance.ContainerPort{
{
Port: to.Int32Ptr(80),
Protocol: containerinstance.ContainerNetworkProtocolTCP,
},
{
Port: to.Int32Ptr(90),
Protocol: containerinstance.ContainerNetworkProtocolTCP,
},
})
assert.DeepEqual(t, groupPports, []containerinstance.Port{
{
Port: to.Int32Ptr(80),
Protocol: containerinstance.TCP,
},
{
Port: to.Int32Ptr(90),
Protocol: containerinstance.TCP,
},
})
}
func TestConvertUDPPortsToAci(t *testing.T) {
service := types.ServiceConfig{
Name: "myService",
Ports: []types.ServicePortConfig{
{
Protocol: "udp",
Target: 80,
Published: 80,
},
},
}
containerPorts, groupPports, _, err := convertPortsToAci(serviceConfigAciHelper(service))
assert.NilError(t, err)
assert.DeepEqual(t, containerPorts, []containerinstance.ContainerPort{
{
Port: to.Int32Ptr(80),
Protocol: containerinstance.ContainerNetworkProtocolUDP,
},
})
assert.DeepEqual(t, groupPports, []containerinstance.Port{
{
Port: to.Int32Ptr(80),
Protocol: containerinstance.UDP,
},
})
}
func TestConvertErrorOnMappingPorts(t *testing.T) {
service := types.ServiceConfig{
Name: "myService",
Ports: []types.ServicePortConfig{
{
Protocol: "",
Target: 80,
Published: 90,
},
},
}
_, _, _, err := convertPortsToAci(serviceConfigAciHelper(service))
assert.Error(t, err, "Port mapping is not supported with ACI, cannot map port 90 to 80 for container myService")
}

查看文件

@ -1,186 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"golang.org/x/oauth2"
"github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
compose "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/login"
)
const (
// Specific username from ACR docs : https://github.com/Azure/acr/blob/master/docs/AAD-OAuth.md#getting-credentials-programatically
tokenUsername = "00000000-0000-0000-0000-000000000000"
dockerHub = "index.docker.io"
)
type registryHelper interface {
getAllRegistryCredentials() (map[string]types.AuthConfig, error)
autoLoginAcr(registry string, loginService login.AzureLoginService) error
}
type cliRegistryHelper struct {
cfg *configfile.ConfigFile
}
func (c cliRegistryHelper) getAllRegistryCredentials() (map[string]types.AuthConfig, error) {
return c.cfg.GetAllCredentials()
}
func newCliRegistryConfLoader() cliRegistryHelper {
return cliRegistryHelper{
cfg: config.LoadDefaultConfigFile(os.Stderr),
}
}
func getRegistryCredentials(project compose.Project, helper registryHelper) ([]containerinstance.ImageRegistryCredential, error) {
loginService, err := login.NewAzureLoginService()
if err != nil {
return nil, err
}
var cloudEnvironment *login.CloudEnvironment
if ce, err := loginService.GetCloudEnvironment(); err != nil {
cloudEnvironment = &ce
}
usedRegistries, acrRegistries := getUsedRegistries(project, cloudEnvironment)
for _, registry := range acrRegistries {
err := helper.autoLoginAcr(registry, loginService)
if err != nil {
fmt.Printf("WARNING: %v\n", err)
fmt.Printf("Could not automatically login to %s from your Azure login. Assuming you already logged in to the ACR registry\n", registry)
}
}
allCreds, err := helper.getAllRegistryCredentials()
if err != nil {
return nil, err
}
var registryCreds []containerinstance.ImageRegistryCredential
for name, oneCred := range allCreds {
parsedURL, err := url.Parse(name)
// Credentials can contain some garbage, we don't return the error here
// because we don't care about these garbage creds.
if err != nil {
continue
}
hostname := parsedURL.Host
if hostname == "" {
hostname = parsedURL.Path
}
if _, ok := usedRegistries[hostname]; ok {
if oneCred.Password != "" {
aciCredential := containerinstance.ImageRegistryCredential{
Server: to.StringPtr(hostname),
Password: to.StringPtr(oneCred.Password),
Username: to.StringPtr(oneCred.Username),
}
registryCreds = append(registryCreds, aciCredential)
} else if oneCred.IdentityToken != "" {
userName := tokenUsername
if oneCred.Username != "" {
userName = oneCred.Username
}
aciCredential := containerinstance.ImageRegistryCredential{
Server: to.StringPtr(hostname),
Password: to.StringPtr(oneCred.IdentityToken),
Username: to.StringPtr(userName),
}
registryCreds = append(registryCreds, aciCredential)
}
}
}
return registryCreds, nil
}
func getUsedRegistries(project compose.Project, ce *login.CloudEnvironment) (map[string]bool, []string) {
usedRegistries := map[string]bool{}
acrRegistries := []string{}
for _, service := range project.Services {
imageName := service.Image
tokens := strings.Split(imageName, "/")
registry := tokens[0]
if len(tokens) == 1 { // ! image names can include "." ...
registry = dockerHub
} else if !strings.Contains(registry, ".") {
registry = dockerHub
} else if ce != nil {
if suffix, present := ce.Suffixes[login.AcrSuffixKey]; present && strings.HasSuffix(registry, suffix) {
acrRegistries = append(acrRegistries, registry)
}
}
usedRegistries[registry] = true
}
return usedRegistries, acrRegistries
}
func (c cliRegistryHelper) autoLoginAcr(registry string, loginService login.AzureLoginService) error {
token, tenantID, err := loginService.GetValidToken()
if err != nil {
return err
}
data := url.Values{
"grant_type": {"access_token"},
"service": {registry},
"tenant": {tenantID},
"access_token": {token.AccessToken},
}
repoAuthURL := fmt.Sprintf("https://%s/oauth2/exchange", registry)
res, err := http.Post(repoAuthURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return errors.Wrap(err, "could not query ACR token")
}
bits, err := ioutil.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, "could not read response body")
}
if res.StatusCode != 200 {
return errors.Errorf("could not obtain ACR token from Azure login, status : %s, response: %s", res.Status, string(bits))
}
newToken := oauth2.Token{}
if err := json.Unmarshal(bits, &newToken); err != nil {
return errors.Wrap(err, "could not read ACR token")
}
cmd := exec.Command("docker", "login", "-u", tokenUsername, "-p", newToken.RefreshToken, registry)
bytes, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, fmt.Sprintf("could not 'docker login' to %s :\n%s\n", registry, string(bytes)))
}
return nil
}

查看文件

@ -1,262 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"errors"
"strconv"
"testing"
"github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
cliconfigtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/compose-cli/aci/login"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
const getAllCredentials = "getAllRegistryCredentials"
const autoLoginAcr = "autoLoginAcr"
func TestHubPrivateImage(t *testing.T) {
registryHelper := &MockRegistryHelper{}
registryHelper.On(getAllCredentials).Return(registry("https://index.docker.io", userPwdCreds("toto", "pwd")), nil)
creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
},
})
}
func TestRegistryNameWithoutProtocol(t *testing.T) {
registryHelper := &MockRegistryHelper{}
registryHelper.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
},
})
}
func TestInvalidCredentials(t *testing.T) {
registryHelper := &MockRegistryHelper{}
registryHelper.On(getAllCredentials).Return(registry("18.195.159.6:444", userPwdCreds("toto", "pwd")), nil)
creds, err := getRegistryCredentials(composeServices("gtardif/privateimg"), registryHelper)
assert.NilError(t, err)
assert.Equal(t, len(creds), 0)
}
func TestImageWithDotInName(t *testing.T) {
registryHelper := &MockRegistryHelper{}
registryHelper.On(getAllCredentials).Return(registry("index.docker.io", userPwdCreds("toto", "pwd")), nil)
creds, err := getRegistryCredentials(composeServices("my.image"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
},
})
}
func TestAcrPrivateImage(t *testing.T) {
registryHelper := &MockRegistryHelper{}
registryHelper.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", tokenCreds("123456")), nil)
registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr("mycontainerregistrygta.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("123456"),
},
})
}
func TestAcrPrivateImageLinux(t *testing.T) {
registryHelper := &MockRegistryHelper{}
token := tokenCreds("123456")
token.Username = tokenUsername
registryHelper.On(getAllCredentials).Return(registry("https://mycontainerregistrygta.azurecr.io", token), nil)
registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr("mycontainerregistrygta.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("123456"),
},
})
}
func TestNoMoreRegistriesThanImages(t *testing.T) {
registryHelper := &MockRegistryHelper{}
configs := map[string]cliconfigtypes.AuthConfig{
"https://mycontainerregistrygta.azurecr.io": tokenCreds("123456"),
"https://index.docker.io": userPwdCreds("toto", "pwd"),
}
registryHelper.On(getAllCredentials).Return(configs, nil)
registryHelper.On(autoLoginAcr, "mycontainerregistrygta.azurecr.io").Return(nil)
creds, err := getRegistryCredentials(composeServices("mycontainerregistrygta.azurecr.io/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr("mycontainerregistrygta.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("123456"),
},
})
creds, err = getRegistryCredentials(composeServices("someuser/privateimg"), registryHelper)
assert.NilError(t, err)
assert.DeepEqual(t, creds, []containerinstance.ImageRegistryCredential{
{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
},
})
}
func TestHubAndSeveralACRRegistries(t *testing.T) {
registryHelper := &MockRegistryHelper{}
configs := map[string]cliconfigtypes.AuthConfig{
"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
"https://mycontainerregistry2.azurecr.io": tokenCreds("456789"),
"https://mycontainerregistry3.azurecr.io": tokenCreds("123456789"),
"https://index.docker.io": userPwdCreds("toto", "pwd"),
"https://other.registry.io": userPwdCreds("user", "password"),
}
registryHelper.On(getAllCredentials).Return(configs, nil)
registryHelper.On(autoLoginAcr, "mycontainerregistry1.azurecr.io").Return(nil)
registryHelper.On(autoLoginAcr, "mycontainerregistry2.azurecr.io").Return(nil)
creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), registryHelper)
assert.NilError(t, err)
assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
Server: to.StringPtr("mycontainerregistry1.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("123456"),
}))
assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
Server: to.StringPtr("mycontainerregistry2.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("456789"),
}))
assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
}))
}
func TestIgnoreACRRegistryFailedAutoLogin(t *testing.T) {
registryHelper := &MockRegistryHelper{}
configs := map[string]cliconfigtypes.AuthConfig{
"https://mycontainerregistry1.azurecr.io": tokenCreds("123456"),
"https://mycontainerregistry3.azurecr.io": tokenCreds("123456789"),
"https://index.docker.io": userPwdCreds("toto", "pwd"),
"https://other.registry.io": userPwdCreds("user", "password"),
}
registryHelper.On(getAllCredentials).Return(configs, nil)
registryHelper.On(autoLoginAcr, "mycontainerregistry1.azurecr.io").Return(nil)
registryHelper.On(autoLoginAcr, "mycontainerregistry2.azurecr.io").Return(errors.New("could not login"))
creds, err := getRegistryCredentials(composeServices("mycontainerregistry1.azurecr.io/privateimg", "someuser/privateImg2", "mycontainerregistry2.azurecr.io/privateimg"), registryHelper)
assert.NilError(t, err)
assert.Equal(t, len(creds), 2)
assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
Server: to.StringPtr("mycontainerregistry1.azurecr.io"),
Username: to.StringPtr(tokenUsername),
Password: to.StringPtr("123456"),
}))
assert.Assert(t, is.Contains(creds, containerinstance.ImageRegistryCredential{
Server: to.StringPtr(dockerHub),
Username: to.StringPtr("toto"),
Password: to.StringPtr("pwd"),
}))
}
func composeServices(images ...string) types.Project {
var services []types.ServiceConfig
for index, name := range images {
service := types.ServiceConfig{
Name: "service" + strconv.Itoa(index),
Image: name,
}
services = append(services, service)
}
return types.Project{
Services: services,
}
}
func registry(host string, configregistryData cliconfigtypes.AuthConfig) map[string]cliconfigtypes.AuthConfig {
return map[string]cliconfigtypes.AuthConfig{
host: configregistryData,
}
}
func userPwdCreds(user string, password string) cliconfigtypes.AuthConfig {
return cliconfigtypes.AuthConfig{
Username: user,
Password: password,
}
}
func tokenCreds(token string) cliconfigtypes.AuthConfig {
return cliconfigtypes.AuthConfig{
IdentityToken: token,
}
}
type MockRegistryHelper struct {
mock.Mock
}
func (s *MockRegistryHelper) getAllRegistryCredentials() (map[string]cliconfigtypes.AuthConfig, error) {
args := s.Called()
return args.Get(0).(map[string]cliconfigtypes.AuthConfig), args.Error(1)
}
func (s *MockRegistryHelper) autoLoginAcr(registry string, loginService login.AzureLoginService) error {
args := s.Called(registry, loginService)
return args.Error(0)
}

查看文件

@ -1,72 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/pkg/errors"
"github.com/docker/compose-cli/api/containers"
)
func (p projectAciHelper) getRestartPolicy() (containerinstance.ContainerGroupRestartPolicy, error) {
var restartPolicyCondition containerinstance.ContainerGroupRestartPolicy
if len(p.Services) >= 1 {
alreadySpecified := false
restartPolicyCondition = containerinstance.Always
for _, service := range p.Services {
if service.Deploy != nil &&
service.Deploy.RestartPolicy != nil {
if !alreadySpecified {
alreadySpecified = true
restartPolicyCondition = toAciRestartPolicy(service.Deploy.RestartPolicy.Condition)
}
if alreadySpecified && restartPolicyCondition != toAciRestartPolicy(service.Deploy.RestartPolicy.Condition) {
return "", errors.New("ACI integration does not support specifying different restart policies on services in the same compose application")
}
}
}
}
return restartPolicyCondition, nil
}
func toAciRestartPolicy(restartPolicy string) containerinstance.ContainerGroupRestartPolicy {
switch restartPolicy {
case containers.RestartPolicyNone:
return containerinstance.Never
case containers.RestartPolicyAny:
return containerinstance.Always
case containers.RestartPolicyOnFailure:
return containerinstance.OnFailure
default:
return containerinstance.Always
}
}
func toContainerRestartPolicy(aciRestartPolicy containerinstance.ContainerGroupRestartPolicy) string {
switch aciRestartPolicy {
case containerinstance.Never:
return containers.RestartPolicyNone
case containerinstance.Always:
return containers.RestartPolicyAny
case containerinstance.OnFailure:
return containers.RestartPolicyOnFailure
default:
return containers.RestartPolicyAny
}
}

查看文件

@ -1,144 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"testing"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/compose-spec/compose-go/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestComposeSingleContainerRestartPolicy(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
RestartPolicy: &types.RestartPolicy{
Condition: "on-failure",
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 1))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
assert.Equal(t, group.RestartPolicy, containerinstance.OnFailure)
}
func TestComposeMultiContainerRestartPolicy(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
RestartPolicy: &types.RestartPolicy{
Condition: "on-failure",
},
},
},
{
Name: "service2",
Image: "image2",
Deploy: &types.DeployConfig{
RestartPolicy: &types.RestartPolicy{
Condition: "on-failure",
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
assert.Equal(t, group.RestartPolicy, containerinstance.OnFailure)
assert.Equal(t, *(*group.Containers)[1].Name, "service2")
assert.Equal(t, group.RestartPolicy, containerinstance.OnFailure)
}
func TestComposeInconsistentMultiContainerRestartPolicy(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Deploy: &types.DeployConfig{
RestartPolicy: &types.RestartPolicy{
Condition: "any",
},
},
},
{
Name: "service2",
Image: "image2",
Deploy: &types.DeployConfig{
RestartPolicy: &types.RestartPolicy{
Condition: "on-failure",
},
},
},
},
}
_, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.Error(t, err, "ACI integration does not support specifying different restart policies on services in the same compose application")
}
func TestComposeSingleContainerGroupToContainerDefaultRestartPolicy(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 1))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
assert.Equal(t, group.RestartPolicy, containerinstance.Always)
}
func TestConvertToAciRestartPolicyCondition(t *testing.T) {
assert.Equal(t, toAciRestartPolicy("none"), containerinstance.Never)
assert.Equal(t, toAciRestartPolicy("always"), containerinstance.Always)
assert.Equal(t, toAciRestartPolicy("on-failure"), containerinstance.OnFailure)
assert.Equal(t, toAciRestartPolicy("on-failure:5"), containerinstance.Always)
}
func TestConvertToDockerRestartPolicyCondition(t *testing.T) {
assert.Equal(t, toContainerRestartPolicy(containerinstance.Never), "none")
assert.Equal(t, toContainerRestartPolicy(containerinstance.Always), "any")
assert.Equal(t, toContainerRestartPolicy(containerinstance.OnFailure), "on-failure")
assert.Equal(t, toContainerRestartPolicy(""), "any")
}

查看文件

@ -1,144 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"encoding/base64"
"fmt"
"io/ioutil"
"path"
"strings"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/pkg/errors"
)
const (
defaultSecretsPath = "/run/secrets"
serviceSecretAbsPathPrefix = "aci-service-secret-path-"
)
func getServiceSecretKey(serviceName, targetDir string) string {
return fmt.Sprintf("%s-%s--%s",
serviceSecretAbsPathPrefix, serviceName, strings.ReplaceAll(targetDir, "/", "-"))
}
func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) {
var secretVolumes []containerinstance.Volume
for _, svc := range p.Services {
squashedTargetVolumes := make(map[string]containerinstance.Volume)
for _, scr := range svc.Secrets {
data, err := ioutil.ReadFile(p.Secrets[scr.Source].File)
if err != nil {
return secretVolumes, err
}
if len(data) == 0 {
continue
}
dataStr := base64.StdEncoding.EncodeToString(data)
if scr.Target == "" {
scr.Target = scr.Source
}
if !path.IsAbs(scr.Target) && strings.ContainsAny(scr.Target, "\\/") {
return []containerinstance.Volume{},
errors.Errorf("in service %q, secret with source %q cannot have a relative path as target. "+
"Only absolute paths are allowed. Found %q",
svc.Name, scr.Source, scr.Target)
}
if !path.IsAbs(scr.Target) {
scr.Target = path.Join(defaultSecretsPath, scr.Target)
}
targetDir := path.Dir(scr.Target)
targetDirKey := getServiceSecretKey(svc.Name, targetDir)
if _, ok := squashedTargetVolumes[targetDir]; !ok {
squashedTargetVolumes[targetDir] = containerinstance.Volume{
Name: to.StringPtr(targetDirKey),
Secret: make(map[string]*string),
}
}
squashedTargetVolumes[targetDir].Secret[path.Base(scr.Target)] = &dataStr
}
for _, v := range squashedTargetVolumes {
secretVolumes = append(secretVolumes, v)
}
}
return secretVolumes, nil
}
func (s serviceConfigAciHelper) getAciSecretsVolumeMounts() ([]containerinstance.VolumeMount, error) {
vms := []containerinstance.VolumeMount{}
presenceSet := make(map[string]bool)
for _, scr := range s.Secrets {
if scr.Target == "" {
scr.Target = scr.Source
}
if !path.IsAbs(scr.Target) {
scr.Target = path.Join(defaultSecretsPath, scr.Target)
}
presenceKey := path.Dir(scr.Target)
if !presenceSet[presenceKey] {
vms = append(vms, containerinstance.VolumeMount{
Name: to.StringPtr(getServiceSecretKey(s.Name, path.Dir(scr.Target))),
MountPath: to.StringPtr(path.Dir(scr.Target)),
ReadOnly: to.BoolPtr(true),
})
presenceSet[presenceKey] = true
}
}
err := validateMountPathCollisions(vms)
if err != nil {
return []containerinstance.VolumeMount{}, err
}
return vms, nil
}
func validateMountPathCollisions(vms []containerinstance.VolumeMount) error {
for i, vm1 := range vms {
for j, vm2 := range vms {
if i == j {
continue
}
var (
biggerVMPath = strings.Split(*vm1.MountPath, "/")
smallerVMPath = strings.Split(*vm2.MountPath, "/")
)
if len(smallerVMPath) > len(biggerVMPath) {
tmp := biggerVMPath
biggerVMPath = smallerVMPath
smallerVMPath = tmp
}
isPrefixed := true
for i := 0; i < len(smallerVMPath); i++ {
if smallerVMPath[i] != biggerVMPath[i] {
isPrefixed = false
break
}
}
if isPrefixed {
return errors.Errorf("mount paths %q and %q collide. A volume mount cannot include another one.", *vm1.MountPath, *vm2.MountPath)
}
}
}
return nil
}

查看文件

@ -1,179 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"github.com/compose-spec/compose-go/types"
"gotest.tools/v3/assert"
)
func TestConvertSecrets(t *testing.T) {
serviceName := "testservice"
secretName := "testsecret"
absBasePath := "/home/user"
tmpFile, err := ioutil.TempFile(os.TempDir(), "TestConvertProjectSecrets-")
assert.NilError(t, err)
_, err = tmpFile.Write([]byte("test content"))
assert.NilError(t, err)
t.Cleanup(func() {
_ = os.Remove(tmpFile.Name())
})
t.Run("mix default and absolute", func(t *testing.T) {
pSquashedDefaultAndAbs := projectAciHelper{
Services: []types.ServiceConfig{
{
Name: serviceName,
Secrets: []types.ServiceSecretConfig{
{
Source: secretName,
Target: "some_target1",
},
{
Source: secretName,
},
{
Source: secretName,
Target: path.Join(defaultSecretsPath, "some_target2"),
},
{
Source: secretName,
Target: path.Join(absBasePath, "some_target3"),
},
{
Source: secretName,
Target: path.Join(absBasePath, "some_target4"),
},
},
},
},
Secrets: map[string]types.SecretConfig{
secretName: {
File: tmpFile.Name(),
},
},
}
volumes, err := pSquashedDefaultAndAbs.getAciSecretVolumes()
assert.NilError(t, err)
assert.Equal(t, len(volumes), 2)
defaultVolumeName := getServiceSecretKey(serviceName, defaultSecretsPath)
homeVolumeName := getServiceSecretKey(serviceName, absBasePath)
// random order since this was created from a map...
for _, vol := range volumes {
switch *vol.Name {
case defaultVolumeName:
assert.Equal(t, len(vol.Secret), 3)
case homeVolumeName:
assert.Equal(t, len(vol.Secret), 2)
default:
assert.Assert(t, false, "unexpected volume name: "+*vol.Name)
}
}
s := serviceConfigAciHelper(pSquashedDefaultAndAbs.Services[0])
vms, err := s.getAciSecretsVolumeMounts()
assert.NilError(t, err)
assert.Equal(t, len(vms), 2)
assert.Equal(t, *vms[0].Name, defaultVolumeName)
assert.Equal(t, *vms[0].MountPath, defaultSecretsPath)
assert.Equal(t, *vms[1].Name, homeVolumeName)
assert.Equal(t, *vms[1].MountPath, absBasePath)
})
t.Run("convert invalid target", func(t *testing.T) {
targetName := "some/invalid/relative/path/target"
pInvalidRelativePathTarget := projectAciHelper{
Services: []types.ServiceConfig{
{
Name: serviceName,
Secrets: []types.ServiceSecretConfig{
{
Source: secretName,
Target: targetName,
},
},
},
},
Secrets: map[string]types.SecretConfig{
secretName: {
File: tmpFile.Name(),
},
},
}
_, err := pInvalidRelativePathTarget.getAciSecretVolumes()
assert.Equal(t, err.Error(),
fmt.Sprintf(`in service %q, secret with source %q cannot have a relative path as target. Only absolute paths are allowed. Found %q`,
serviceName, secretName, targetName))
})
t.Run("convert colliding default targets", func(t *testing.T) {
targetName1 := path.Join(defaultSecretsPath, "target1")
targetName2 := path.Join(defaultSecretsPath, "sub/folder/target2")
service := serviceConfigAciHelper{
Name: serviceName,
Secrets: []types.ServiceSecretConfig{
{
Source: secretName,
Target: targetName1,
},
{
Source: secretName,
Target: targetName2,
},
},
}
_, err := service.getAciSecretsVolumeMounts()
assert.Equal(t, err.Error(),
fmt.Sprintf(`mount paths %q and %q collide. A volume mount cannot include another one.`,
path.Dir(targetName1), path.Dir(targetName2)))
})
t.Run("convert colliding absolute targets", func(t *testing.T) {
targetName1 := path.Join(absBasePath, "target1")
targetName2 := path.Join(absBasePath, "sub/folder/target2")
service := serviceConfigAciHelper{
Name: serviceName,
Secrets: []types.ServiceSecretConfig{
{
Source: secretName,
Target: targetName1,
},
{
Source: secretName,
Target: targetName2,
},
},
}
_, err := service.getAciSecretsVolumeMounts()
assert.Equal(t, err.Error(),
fmt.Sprintf(`mount paths %q and %q collide. A volume mount cannot include another one.`,
path.Dir(targetName1), path.Dir(targetName2)))
})
}

查看文件

@ -1,177 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/pkg/api"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
)
const (
// AzureFileDriverName driver name for azure file share
AzureFileDriverName = "azure_file"
// VolumeDriveroptsShareNameKey driver opt for fileshare name
VolumeDriveroptsShareNameKey = "share_name"
// VolumeDriveroptsAccountNameKey driver opt for storage account
VolumeDriveroptsAccountNameKey = "storage_account_name"
volumeReadOnly = "read_only"
)
func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) ([]containerinstance.Volume, error) {
var azureFileVolumesSlice []containerinstance.Volume
for name, v := range p.Volumes {
if v.Driver == AzureFileDriverName {
shareName, ok := v.DriverOpts[VolumeDriveroptsShareNameKey]
if !ok {
return nil, fmt.Errorf("cannot retrieve fileshare name for Azurefile")
}
accountName, ok := v.DriverOpts[VolumeDriveroptsAccountNameKey]
if !ok {
return nil, fmt.Errorf("cannot retrieve account name for Azurefile")
}
readOnly, ok := v.DriverOpts[volumeReadOnly]
if !ok {
readOnly = "false"
}
ro, err := strconv.ParseBool(readOnly)
if err != nil {
return nil, fmt.Errorf("invalid mode %q for volume", readOnly)
}
accountKey, err := helper.GetAzureStorageAccountKey(ctx, accountName)
if err != nil {
return nil, err
}
aciVolume := containerinstance.Volume{
Name: to.StringPtr(name),
AzureFile: &containerinstance.AzureFileVolume{
ShareName: to.StringPtr(shareName),
StorageAccountName: to.StringPtr(accountName),
StorageAccountKey: to.StringPtr(accountKey),
ReadOnly: &ro,
},
}
azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume)
}
}
return azureFileVolumesSlice, nil
}
func (s serviceConfigAciHelper) getAciFileVolumeMounts() ([]containerinstance.VolumeMount, error) {
var aciServiceVolumes []containerinstance.VolumeMount
for _, sv := range s.Volumes {
if sv.Type == string(types.VolumeTypeBind) {
return []containerinstance.VolumeMount{}, fmt.Errorf("host path (%q) not allowed as volume source, you need to reference an Azure File Share defined in the 'volumes' section", sv.Source)
}
aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{
Name: to.StringPtr(sv.Source),
MountPath: to.StringPtr(sv.Target),
})
}
return aciServiceVolumes, nil
}
// GetRunVolumes return volume configurations for a project and a single service
// this is meant to be used as a compose project of a single service
func GetRunVolumes(volumes []string) (map[string]types.VolumeConfig, []types.ServiceVolumeConfig, error) {
var serviceConfigVolumes []types.ServiceVolumeConfig
projectVolumes := make(map[string]types.VolumeConfig, len(volumes))
for i, v := range volumes {
var vi volumeInput
err := vi.parse(fmt.Sprintf("volume-%d", i), v)
if err != nil {
return nil, nil, err
}
readOnly := strconv.FormatBool(vi.readonly)
projectVolumes[vi.name] = types.VolumeConfig{
Name: vi.name,
Driver: AzureFileDriverName,
DriverOpts: map[string]string{
VolumeDriveroptsAccountNameKey: vi.storageAccount,
VolumeDriveroptsShareNameKey: vi.fileshare,
volumeReadOnly: readOnly,
},
}
sv := types.ServiceVolumeConfig{
Type: AzureFileDriverName,
Source: vi.name,
Target: vi.target,
ReadOnly: vi.readonly,
}
serviceConfigVolumes = append(serviceConfigVolumes, sv)
}
return projectVolumes, serviceConfigVolumes, nil
}
type volumeInput struct {
name string
storageAccount string
fileshare string
target string
readonly bool
}
// parse takes a candidate string and creates a volumeInput
// Candidates take the form of <source>[:<target>][:<permissions>]
// Source is of the form `<storage account>/<fileshare>`
// If only the source is specified then the target is set to `/run/volumes/<fileshare>`
// Target is an absolute path in the container of the form `/path/to/mount`
// Permissions can only be set if the target is set
// If set, permissions must be `rw` or `ro`
func (v *volumeInput) parse(name string, candidate string) error {
v.name = name
tokens := strings.Split(candidate, ":")
sourceTokens := strings.Split(tokens[0], "/")
if len(sourceTokens) != 2 || sourceTokens[0] == "" {
return errors.Wrapf(api.ErrParsingFailed, "volume specification %q does not include a storage account before '/'", candidate)
}
if sourceTokens[1] == "" {
return errors.Wrapf(api.ErrParsingFailed, "volume specification %q does not include a storage file fileshare after '/'", candidate)
}
v.storageAccount = sourceTokens[0]
v.fileshare = sourceTokens[1]
switch len(tokens) {
case 1: // source only
v.target = "/run/volumes/" + v.fileshare
case 2: // source and target
v.target = tokens[1]
case 3: // source, target, and permissions
v.target = tokens[1]
permissions := strings.ToLower(tokens[2])
if permissions != "ro" && permissions != "rw" {
return errors.Wrapf(api.ErrParsingFailed, "volume specification %q has an invalid mode %q", candidate, permissions)
}
v.readonly = permissions == "ro"
default:
return errors.Wrapf(api.ErrParsingFailed, "volume specification %q has invalid format", candidate)
}
return nil
}

查看文件

@ -1,215 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 convert
import (
"context"
"strconv"
"testing"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGetRunVolumes(t *testing.T) {
volumeStrings := []string{
"myuser1/myshare1:/my/path/to/target1",
"myuser2/myshare2:/my/path/to/target2:ro",
"myuser3/myshare3:/my/path/to/target3:rw",
"myuser4/mydefaultsharename", // Use default placement at '/run/volumes/<share_name>'
}
var goldenVolumeConfigs = map[string]types.VolumeConfig{
"volume-0": getAzurefileVolumeConfig("volume-0", "myuser1", "myshare1", false),
"volume-1": getAzurefileVolumeConfig("volume-1", "myuser2", "myshare2", true),
"volume-2": getAzurefileVolumeConfig("volume-2", "myuser3", "myshare3", false),
"volume-3": getAzurefileVolumeConfig("volume-3", "myuser4", "mydefaultsharename", false),
}
goldenServiceVolumeConfigs := []types.ServiceVolumeConfig{
getServiceVolumeConfig("volume-0", "/my/path/to/target1", false),
getServiceVolumeConfig("volume-1", "/my/path/to/target2", true),
getServiceVolumeConfig("volume-2", "/my/path/to/target3", false),
getServiceVolumeConfig("volume-3", "/run/volumes/mydefaultsharename", false),
}
volumeConfigs, serviceVolumeConfigs, err := GetRunVolumes(volumeStrings)
assert.NilError(t, err)
for k, v := range volumeConfigs {
assert.DeepEqual(t, goldenVolumeConfigs[k], v)
}
for i, v := range serviceVolumeConfigs {
assert.DeepEqual(t, goldenServiceVolumeConfigs[i], v)
}
}
func TestGetRunVolumesMissingFileShare(t *testing.T) {
_, _, err := GetRunVolumes([]string{"myaccount/"})
assert.ErrorContains(t, err, "does not include a storage file fileshare after '/'")
}
func TestGetRunVolumesMissingUser(t *testing.T) {
_, _, err := GetRunVolumes([]string{"/myshare"})
assert.ErrorContains(t, err, "does not include a storage account before '/'")
}
func TestGetRunVolumesNoShare(t *testing.T) {
_, _, err := GetRunVolumes([]string{"noshare"})
assert.ErrorContains(t, err, "does not include a storage account before '/'")
}
func TestGetRunVolumesInvalidOption(t *testing.T) {
_, _, err := GetRunVolumes([]string{"myuser4/myshare4:/my/path/to/target4:invalid"})
assert.ErrorContains(t, err, `volume specification "myuser4/myshare4:/my/path/to/target4:invalid" has an invalid mode "invalid"`)
}
func TestComposeVolumes(t *testing.T) {
ctx := context.TODO()
accountName := "myAccount"
mockStorageHelper.On("GetAzureStorageAccountKey", ctx, accountName).Return("123456", nil)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
},
},
Volumes: types.Volumes{
"vol1": types.VolumeConfig{
Driver: "azure_file",
DriverOpts: map[string]string{
"share_name": "myFileshare",
"storage_account_name": accountName,
},
},
},
}
group, err := ToContainerGroup(ctx, convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 1))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
expectedGroupVolume := containerinstance.Volume{
Name: to.StringPtr("vol1"),
AzureFile: &containerinstance.AzureFileVolume{
ShareName: to.StringPtr("myFileshare"),
StorageAccountName: &accountName,
StorageAccountKey: to.StringPtr("123456"),
ReadOnly: to.BoolPtr(false),
},
}
assert.Equal(t, len(*group.Volumes), 1)
assert.DeepEqual(t, (*group.Volumes)[0], expectedGroupVolume)
}
func TestPathVolumeErrorMessage(t *testing.T) {
ctx := context.TODO()
accountName := "myAccount"
mockStorageHelper.On("GetAzureStorageAccountKey", ctx, accountName).Return("123456", nil)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Volumes: []types.ServiceVolumeConfig{
{
Source: "/path",
Target: "/target",
Type: string(types.VolumeTypeBind),
},
},
},
},
}
_, err := ToContainerGroup(ctx, convertCtx, project, mockStorageHelper)
assert.ErrorContains(t, err, `host path ("/path") not allowed as volume source, you need to reference an Azure File Share defined in the 'volumes' section`)
}
func TestComposeVolumesRO(t *testing.T) {
ctx := context.TODO()
accountName := "myAccount"
mockStorageHelper.On("GetAzureStorageAccountKey", ctx, accountName).Return("123456", nil)
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
},
},
Volumes: types.Volumes{
"vol1": types.VolumeConfig{
Driver: "azure_file",
DriverOpts: map[string]string{
"share_name": "myFileshare",
"storage_account_name": accountName,
"read_only": "true",
},
},
},
}
group, err := ToContainerGroup(ctx, convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 1))
assert.Equal(t, *(*group.Containers)[0].Name, "service1")
expectedGroupVolume := containerinstance.Volume{
Name: to.StringPtr("vol1"),
AzureFile: &containerinstance.AzureFileVolume{
ShareName: to.StringPtr("myFileshare"),
StorageAccountName: &accountName,
StorageAccountKey: to.StringPtr("123456"),
ReadOnly: to.BoolPtr(true),
},
}
assert.Equal(t, len(*group.Volumes), 1)
assert.DeepEqual(t, (*group.Volumes)[0], expectedGroupVolume)
}
type mockStorageLogin struct {
mock.Mock
}
func (s *mockStorageLogin) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
args := s.Called(ctx, accountName)
return args.String(0), args.Error(1)
}
func getServiceVolumeConfig(source string, target string, readOnly bool) types.ServiceVolumeConfig {
return types.ServiceVolumeConfig{
Type: "azure_file",
Source: source,
Target: target,
ReadOnly: readOnly,
}
}
func getAzurefileVolumeConfig(name string, accountNameKey string, shareNameKey string, readOnly bool) types.VolumeConfig {
return types.VolumeConfig{
Name: name,
Driver: "azure_file",
DriverOpts: map[string]string{
VolumeDriveroptsAccountNameKey: accountNameKey,
VolumeDriveroptsShareNameKey: shareNameKey,
volumeReadOnly: strconv.FormatBool(readOnly),
},
}
}

查看文件

@ -1,24 +0,0 @@
services:
db:
build: db
image: gtardif/sentences-db
words:
build: words
image: gtardif/sentences-api
web:
build: web
image: gtardif/sentences-web
ports:
- "80:80"
secrets:
- source: mysecret1
target: mytarget1
- mysecret2
secrets:
mysecret1:
file: ./my_secret1.txt
mysecret2:
file: ./my_secret2.txt

查看文件

@ -1,23 +0,0 @@
services:
db:
build: db
image: gtardif/sentences-db
words:
build: words
image: gtardif/sentences-api
web:
build: web
image: gtardif/sentences-web
ports:
- "80:80"
volumes:
- mydata:/static/volume_test
volumes:
mydata:
driver: azure_file
driver_opts:
share_name: dockertestshare
storage_account_name: dockertestvolumeaccount

查看文件

@ -1,16 +0,0 @@
# Copyright 2020 Docker Compose CLI 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.
FROM postgres:10.0-alpine
COPY words.sql /docker-entrypoint-initdb.d/

查看文件

@ -1,55 +0,0 @@
CREATE TABLE nouns (word TEXT NOT NULL);
CREATE TABLE verbs (word TEXT NOT NULL);
CREATE TABLE adjectives (word TEXT NOT NULL);
INSERT INTO nouns(word) VALUES
('cloud'),
('elephant'),
('gø language'),
('laptøp'),
('cøntainer'),
('micrø-service'),
('turtle'),
('whale'),
('gøpher'),
('møby døck'),
('server'),
('bicycle'),
('viking'),
('mermaid'),
('fjørd'),
('legø'),
('flødebolle'),
('smørrebrød');
INSERT INTO verbs(word) VALUES
('will drink'),
('smashes'),
('smøkes'),
('eats'),
('walks tøwards'),
('løves'),
('helps'),
('pushes'),
('debugs'),
('invites'),
('hides'),
('will ship');
INSERT INTO adjectives(word) VALUES
('the exquisite'),
('a pink'),
('the røtten'),
('a red'),
('the serverless'),
('a brøken'),
('a shiny'),
('the pretty'),
('the impressive'),
('an awesøme'),
('the famøus'),
('a gigantic'),
('the gløriøus'),
('the nørdic'),
('the welcøming'),
('the deliciøus');

查看文件

@ -1,16 +0,0 @@
services:
db:
build: aci-demo/db
image: gtardif/sentences-db
service1:
container_name: words
build: aci-demo/words
image: gtardif/sentences-api
ports:
- "8080:8080"
web:
build: aci-demo/web
image: gtardif/sentences-web
ports:
- "80:80"

查看文件

@ -1,42 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 133.21 53.15">
<defs>
<style>
.cls-1, .cls-2 {
fill: #fff;
}
.cls-2 {
fill-rule: evenodd;
}
.cls-3 {
fill: #ffcd34;
}
</style>
</defs>
<g id="Symbol_12_1" data-name="Symbol 12 – 1" transform="translate(-152.5 -26.85)">
<path id="Path_16961" data-name="Path 16961" class="cls-1" d="M624.935,21.191a9.008,9.008,0,0,0-12.844,0,9.155,9.155,0,0,0,0,12.966,9.113,9.113,0,0,0,15.535-6.483,9.635,9.635,0,0,0-.673-3.547A9.03,9.03,0,0,0,624.935,21.191Zm-1.162,8.746a7.394,7.394,0,0,1-1.223,1.835A4.839,4.839,0,0,1,620.715,33a5.276,5.276,0,0,1-2.2.489A5.789,5.789,0,0,1,616.25,33a6.111,6.111,0,0,1-1.774-1.223,5.67,5.67,0,0,1-1.223-1.835,5.436,5.436,0,0,1-.428-2.2,5.242,5.242,0,0,1,.428-2.2,6.024,6.024,0,0,1,1.223-1.835,6.11,6.11,0,0,1,1.774-1.223,5.561,5.561,0,0,1,2.263-.489,5.276,5.276,0,0,1,2.2.489A7.393,7.393,0,0,1,622.55,23.7a7.394,7.394,0,0,1,1.223,1.835,5.243,5.243,0,0,1,.428,2.2A5.641,5.641,0,0,1,623.773,29.937Z" transform="translate(-436.778 16.24)"/>
<path id="Path_16962" data-name="Path 16962" class="cls-1" d="M593.014,5.6A1.672,1.672,0,0,0,591.3,7.313v8.2a9.015,9.015,0,0,0-12.11.673,9.155,9.155,0,0,0,0,12.966,9.008,9.008,0,0,0,12.844,0,8.817,8.817,0,0,0,2.691-6.483V7.313A1.672,1.672,0,0,0,593.014,5.6Zm-2.141,19.327a7.393,7.393,0,0,1-1.223,1.835,4.838,4.838,0,0,1-1.835,1.223,5.276,5.276,0,0,1-2.2.489,5.789,5.789,0,0,1-2.263-.489,6.111,6.111,0,0,1-1.774-1.223,5.669,5.669,0,0,1-1.223-1.835,5.436,5.436,0,0,1-.428-2.2,5.242,5.242,0,0,1,.428-2.2,6.023,6.023,0,0,1,1.223-1.835,6.11,6.11,0,0,1,1.774-1.223,5.561,5.561,0,0,1,2.263-.489,5.276,5.276,0,0,1,2.2.489,7.391,7.391,0,0,1,1.835,1.223,7.393,7.393,0,0,1,1.223,1.835,5.243,5.243,0,0,1,.428,2.2A5.641,5.641,0,0,1,590.873,24.927Z" transform="translate(-424 21.25)"/>
<path id="Path_16963" data-name="Path 16963" class="cls-1" d="M746.139,21.5a1.209,1.209,0,0,0,.122-.673,1.523,1.523,0,0,0-.428-1.162,2.441,2.441,0,0,0-1.162-.673,6.211,6.211,0,0,0-1.529-.367,13.979,13.979,0,0,0-1.529-.122,8.477,8.477,0,0,0-3.058.55,8.717,8.717,0,0,0-2.63,1.529v-.306a1.818,1.818,0,0,0-.489-1.223,1.708,1.708,0,0,0-1.223-.489,1.818,1.818,0,0,0-1.223.489,1.708,1.708,0,0,0-.489,1.223V35.258a1.818,1.818,0,0,0,.489,1.223,1.708,1.708,0,0,0,1.223.489,1.818,1.818,0,0,0,1.223-.489,1.708,1.708,0,0,0,.489-1.223V27.674a5.729,5.729,0,0,1,.428-2.263,7.391,7.391,0,0,1,1.223-1.835,5.668,5.668,0,0,1,1.835-1.223,5.876,5.876,0,0,1,4.4,0,1.629,1.629,0,0,0,.734.183,2.2,2.2,0,0,0,.673-.122,1.445,1.445,0,0,0,.55-.367Z" transform="translate(-484.588 16.24)"/>
<path id="Path_16964" data-name="Path 16964" class="cls-1" d="M715.235,21.191a9.008,9.008,0,0,0-12.844,0,9.155,9.155,0,0,0,0,12.966,9.048,9.048,0,0,0,12.355.489,1.818,1.818,0,0,0,.489-1.223,1.672,1.672,0,0,0-1.713-1.713,1.758,1.758,0,0,0-1.1.428,4.553,4.553,0,0,1-1.651.979,5.064,5.064,0,0,1-1.957.306,6.406,6.406,0,0,1-1.835-.306,5.741,5.741,0,0,1-1.59-.856,6.392,6.392,0,0,1-1.284-1.284,5.168,5.168,0,0,1-.8-1.651h12.844a1.818,1.818,0,0,0,1.223-.489,1.708,1.708,0,0,0,.489-1.223,9.636,9.636,0,0,0-.673-3.547A9.609,9.609,0,0,0,715.235,21.191Zm-11.927,4.771a5.168,5.168,0,0,1,.8-1.651,6.39,6.39,0,0,1,1.284-1.284,7.979,7.979,0,0,1,1.59-.856,6.005,6.005,0,0,1,1.774-.306,5.682,5.682,0,0,1,1.774.306,5.74,5.74,0,0,1,1.59.856A6.388,6.388,0,0,1,713.4,24.31a5.168,5.168,0,0,1,.8,1.651Z" transform="translate(-471.849 16.24)"/>
<path id="Path_16965" data-name="Path 16965" class="cls-1" d="M686.68,15.2a2.6,2.6,0,0,0-.122-.673l-.367-.55a.891.891,0,0,0-.55-.367,2.2,2.2,0,0,0-.673-.122,1.726,1.726,0,0,0-.917.245L674.325,20.1V7.313A1.672,1.672,0,0,0,672.613,5.6a1.819,1.819,0,0,0-1.223.489,1.708,1.708,0,0,0-.489,1.223V30.126a1.818,1.818,0,0,0,.489,1.223,1.491,1.491,0,0,0,1.223.489,1.672,1.672,0,0,0,1.712-1.713V24.193l1.957-1.284,7.523,8.5a1.522,1.522,0,0,0,1.162.428,2.2,2.2,0,0,0,.673-.122,1.444,1.444,0,0,0,.55-.367l.367-.55a1.21,1.21,0,0,0,.122-.673,1.942,1.942,0,0,0-.489-1.223l-6.972-7.951,6.789-4.465A1.374,1.374,0,0,0,686.68,15.2Z" transform="translate(-460.663 21.25)"/>
<path id="Path_16966" data-name="Path 16966" class="cls-1" d="M647.338,23.676a6.418,6.418,0,0,1,1.835-1.223,5.243,5.243,0,0,1,2.2-.428,5.071,5.071,0,0,1,1.957.367,7.128,7.128,0,0,1,1.713,1.04,1.718,1.718,0,0,0,2.813-1.346,1.616,1.616,0,0,0-.612-1.284,9.048,9.048,0,0,0-12.355.489,9.155,9.155,0,0,0,0,12.966,9.048,9.048,0,0,0,12.355.489,1.76,1.76,0,0,0,.55-1.284,1.743,1.743,0,0,0-2.813-1.346,5.6,5.6,0,0,1-1.713,1.04,5.3,5.3,0,0,1-1.957.367,5.242,5.242,0,0,1-2.2-.428,7.393,7.393,0,0,1-1.835-1.223,5.669,5.669,0,0,1-1.223-1.835,5.529,5.529,0,0,1-.428-2.263,5.729,5.729,0,0,1,.428-2.263A4.97,4.97,0,0,1,647.338,23.676Z" transform="translate(-449.517 16.202)"/>
<path id="Path_16967" data-name="Path 16967" class="cls-2" d="M599.516,62.127h3.853V58.64h-3.853v3.486Zm-4.526,0h3.853V58.64H594.99v3.486Zm-4.587,0h3.853V58.64H590.4v3.486Zm-4.587,0h3.853V58.64h-3.853v3.486Zm-4.526,0h3.853V58.64H581.29v3.486Zm4.526-4.22h3.853V54.359h-3.853v3.547Zm4.587,0h3.853V54.359H590.4v3.547Zm4.587,0h3.853V54.359H594.99v3.547Zm0-4.22h3.853V50.2H594.99v3.486Zm20.061,6.361a6.849,6.849,0,0,0-4.281-.489,5.669,5.669,0,0,0-2.385-3.731l-.8-.55-.55.8a6.435,6.435,0,0,0-.917,3.914,4.451,4.451,0,0,0,.734,2.141,5.8,5.8,0,0,1-2.813.612H578.6l-.061.306a12.277,12.277,0,0,0,2.63,9.174c2.2,2.569,5.443,3.853,9.664,3.853,9.235,0,16.024-4.22,19.2-11.927,1.284,0,3.976,0,5.321-2.63a6.71,6.71,0,0,0,.367-.734l.122-.306-.8-.428Z" transform="translate(-424.753 3.929)"/>
<path id="Path_16968" data-name="Path 16968" class="cls-1" d="M712.514,58.268a7.331,7.331,0,0,1,4.832-1.468,8.31,8.31,0,0,1,3.242.673,7.526,7.526,0,0,1,2.691,1.835A8.1,8.1,0,0,1,725.114,62a8.612,8.612,0,0,1,.673,3.3v8.073A1.819,1.819,0,0,1,725.3,74.6a1.708,1.708,0,0,1-1.223.489,1.818,1.818,0,0,1-1.223-.489,1.708,1.708,0,0,1-.489-1.223V65.3a4.349,4.349,0,0,0-.428-1.957,4.889,4.889,0,0,0-1.1-1.651,4.357,4.357,0,0,0-1.651-1.1,5.355,5.355,0,0,0-1.957-.428,4.178,4.178,0,0,0-1.957.428,5.059,5.059,0,0,0-2.752,2.752,5.354,5.354,0,0,0-.428,1.957v8.073A1.818,1.818,0,0,1,711.6,74.6a1.708,1.708,0,0,1-1.223.489,1.818,1.818,0,0,1-1.223-.489,1.759,1.759,0,0,1-.55-1.223v-14.8a2.039,2.039,0,0,1,.55-1.284,1.491,1.491,0,0,1,1.223-.489,1.818,1.818,0,0,1,1.223.489A3.5,3.5,0,0,1,712.514,58.268Z" transform="translate(-475.305 1.365)"/>
<path id="Path_16969" data-name="Path 16969" class="cls-1" d="M651.915,61.676a6.024,6.024,0,0,1,1.835-1.223,5.529,5.529,0,0,1,2.263-.428,5.388,5.388,0,0,1,2.018.367,7.013,7.013,0,0,1,1.774,1.1,1.718,1.718,0,0,0,2.813-1.346,1.537,1.537,0,0,0-.612-1.284,8.753,8.753,0,0,0-5.933-2.263,9.255,9.255,0,0,0-6.483,15.78,8.817,8.817,0,0,0,6.483,2.691,8.47,8.47,0,0,0,5.933-2.263,1.76,1.76,0,0,0,.55-1.284,1.743,1.743,0,0,0-2.813-1.346,4.612,4.612,0,0,1-1.713,1.04,5.387,5.387,0,0,1-2.018.367,5.728,5.728,0,0,1-2.263-.428,7.392,7.392,0,0,1-1.835-1.223,6.022,6.022,0,0,1-1.223-1.835,5.528,5.528,0,0,1-.428-2.263,5.728,5.728,0,0,1,.428-2.263A5.619,5.619,0,0,1,651.915,61.676Z" transform="translate(-451.342 1.443)"/>
<path id="Path_16970" data-name="Path 16970" class="cls-1" d="M691.174,59.591A9.126,9.126,0,0,0,678.33,72.557,9.052,9.052,0,0,0,693.8,66.074a9.083,9.083,0,0,0-.673-3.486A7.511,7.511,0,0,0,691.174,59.591Zm-1.162,8.685a6.024,6.024,0,0,1-1.223,1.835,4.84,4.84,0,0,1-1.835,1.223,5.242,5.242,0,0,1-2.2.428,5.728,5.728,0,0,1-2.263-.428,6.111,6.111,0,0,1-1.774-1.223,5.668,5.668,0,0,1-1.223-1.835,5.436,5.436,0,0,1-.428-2.2,5.242,5.242,0,0,1,.428-2.2,6.022,6.022,0,0,1,1.223-1.835,6.11,6.11,0,0,1,1.774-1.223,5.339,5.339,0,0,1,2.263-.428,5.242,5.242,0,0,1,2.2.428,7.394,7.394,0,0,1,1.835,1.223,6.418,6.418,0,0,1,1.223,1.835,5.243,5.243,0,0,1,.428,2.2A5.641,5.641,0,0,1,690.012,68.276Z" transform="translate(-462.528 1.326)"/>
<g id="Group_7157" data-name="Group 7157" transform="translate(254.885 46.545)">
<g id="Group_7154" data-name="Group 7154" transform="translate(2.569 12.538)">
<path id="Path_16971" data-name="Path 16971" class="cls-1" d="M752.565,64.049a.428.428,0,0,1-.306.734h-3.731a.418.418,0,0,1-.428-.428V58.728a.418.418,0,0,1,.428-.428h3.731a.418.418,0,0,1,.428.428c0,.122-.061.183-.122.306a.467.467,0,0,1-.306.122h-3.3v2.018h2.385a.418.418,0,0,1,.428.428c0,.122-.061.183-.122.306a.467.467,0,0,1-.306.122h-2.385v2.08h3.3C752.381,63.927,752.443,63.988,752.565,64.049Z" transform="translate(-748.1 -58.3)"/>
<path id="Path_16972" data-name="Path 16972" class="cls-1" d="M756.645,58.422a.428.428,0,0,1,.734.306v3.853a1.229,1.229,0,0,0,.245.8,1.5,1.5,0,0,0,.673.55,2.536,2.536,0,0,0,.979.183,2.042,2.042,0,0,0,.917-.183,1.339,1.339,0,0,0,.612-.55,1.735,1.735,0,0,0,.245-.8V58.728a.428.428,0,1,1,.856,0v3.853a2.342,2.342,0,0,1-.306,1.162,2.286,2.286,0,0,1-.917.8,3.114,3.114,0,0,1-1.346.306,3.4,3.4,0,0,1-1.407-.306,2.551,2.551,0,0,1-.979-.8,2.014,2.014,0,0,1-.367-1.162V58.728A.269.269,0,0,1,756.645,58.422Z" transform="translate(-751.385 -58.3)"/>
</g>
<g id="Group_7156" data-name="Group 7156">
<path id="Path_16973" data-name="Path 16973" class="cls-3" d="M782.169,54.008a8.421,8.421,0,0,1,1.1,4.281,9.836,9.836,0,0,1-1.1,4.648,8.118,8.118,0,0,1-3.119,3.3,8.529,8.529,0,0,1-4.4,1.223,8.692,8.692,0,0,1-4.465-1.162,8.356,8.356,0,0,1-3.119-3.242,9.7,9.7,0,0,1-1.162-4.648,8.815,8.815,0,0,1,1.1-4.342,8.973,8.973,0,0,1,2.936-3.119,7.284,7.284,0,0,1-2.08-2.446,7.136,7.136,0,0,1-.734-3.18,7.529,7.529,0,0,1,.979-3.853,7.107,7.107,0,0,1,2.691-2.691,7.529,7.529,0,0,1,3.853-.979,6.941,6.941,0,0,1,3.731.979,6.793,6.793,0,0,1,2.63,2.63,7.766,7.766,0,0,1,.979,3.792,7.207,7.207,0,0,1-.734,3.242,6.652,6.652,0,0,1-2.08,2.446A8.14,8.14,0,0,1,782.169,54.008Zm-4.1.612a4.877,4.877,0,0,0-7.034.061,5.637,5.637,0,0,0-1.407,3.853,5.871,5.871,0,0,0,.612,2.752,5.185,5.185,0,0,0,1.774,2.018,4.651,4.651,0,0,0,5.015,0,4.967,4.967,0,0,0,1.774-2.079,6.417,6.417,0,0,0,.673-2.875A4.978,4.978,0,0,0,778.071,54.619Zm-6.116-6.483a3.5,3.5,0,0,0,5.26-.061,4.221,4.221,0,0,0,1.04-2.936,3.941,3.941,0,0,0-1.04-2.752,3.462,3.462,0,0,0-2.63-1.1,3.6,3.6,0,0,0-2.691,1.1,3.879,3.879,0,0,0-1.04,2.875A4.123,4.123,0,0,0,771.955,48.136Z" transform="translate(-752.444 -37.8)"/>
<g id="Group_7155" data-name="Group 7155" transform="translate(0 0.122)">
<path id="Path_16974" data-name="Path 16974" class="cls-3" d="M753.5,71.7v7.034a1.88,1.88,0,0,0,1.9,1.9,1.937,1.937,0,0,0,1.407-.55,1.9,1.9,0,0,0,.55-1.346V71.7Z" transform="translate(-747.628 -51.089)"/>
<path id="Path_16975" data-name="Path 16975" class="cls-3" d="M745.8,43.994a3.11,3.11,0,0,0,1.223-.245l2.752-1.162v6.3h3.853v-8.93a1.938,1.938,0,0,0-.55-1.407,1.8,1.8,0,0,0-1.346-.55,4.516,4.516,0,0,0-.734.122l-5.872,2.141a1.807,1.807,0,0,0-.917.673,2.112,2.112,0,0,0-.306,1.1,1.8,1.8,0,0,0,.55,1.346A1.574,1.574,0,0,0,745.8,43.994Z" transform="translate(-743.9 -38)"/>
</g>
</g>
</g>
</g>
</svg>

之前

宽度:  |  高度:  |  大小: 11 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 67 KiB

查看文件

@ -1 +0,0 @@
myPassword1

查看文件

@ -1 +0,0 @@
another_password

查看文件

@ -1,23 +0,0 @@
# Copyright 2020 Docker Compose CLI 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.
# BUILD
FROM golang:1.16-alpine AS build
COPY dispatcher.go .
RUN mkdir -p /out && go build -o /out/dispatcher dispatcher.go
FROM alpine AS run
EXPOSE 80
CMD ["/dispatcher"]
COPY static /static/
COPY --from=build /out/dispatcher /dispatcher

查看文件

@ -1,85 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
fwd := &forwarder{"words", 8080}
http.Handle("/words/", http.StripPrefix("/words", fwd))
http.Handle("/", http.FileServer(http.Dir("static")))
fmt.Println("Listening on port 80")
err := http.ListenAndServe(":80", nil)
if err != nil {
fmt.Printf("Error while starting server: %v", err)
}
}
type forwarder struct {
host string
port int
}
func (f *forwarder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
addrs, err := net.LookupHost(f.host)
if err != nil {
log.Println("Error", err)
http.Error(w, err.Error(), 500)
return
}
log.Printf("%s %d available ips: %v", r.URL.Path, len(addrs), addrs)
ip := addrs[rand.Intn(len(addrs))]
log.Printf("%s I choose %s", r.URL.Path, ip)
url := fmt.Sprintf("http://%s:%d%s", ip, f.port, r.URL.Path)
log.Printf("%s Calling %s", r.URL.Path, url)
if err = copy(url, ip, w); err != nil {
log.Println("Error", err)
http.Error(w, err.Error(), 500)
return
}
}
func copy(url, ip string, w http.ResponseWriter) error {
resp, err := http.Get(url)
if err != nil {
return err
}
w.Header().Set("source", ip)
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
_, err = w.Write(buf)
return err
}

查看文件

@ -1,311 +0,0 @@
/*
AngularJS v1.5.3
(c) 2010-2016 Google, Inc. http://angularjs.org
License: MIT
*/
(function(T,P,u){'use strict';function O(a){return function(){var b=arguments[0],d;d="["+(a?a+":":"")+b+"] http://errors.angularjs.org/1.5.3/"+(a?a+"/":"")+b;for(b=1;b<arguments.length;b++){d=d+(1==b?"?":"&")+"p"+(b-1)+"=";var c=encodeURIComponent,e;e=arguments[b];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;d+=c(e)}return Error(d)}}function za(a){if(null==a||Ya(a))return!1;if(M(a)||y(a)||H&&a instanceof H)return!0;
var b="length"in Object(a)&&a.length;return R(b)&&(0<=b&&(b-1 in a||a instanceof Array)||"function"==typeof a.item)}function q(a,b,d){var c,e;if(a)if(D(a))for(c in a)"prototype"==c||"length"==c||"name"==c||a.hasOwnProperty&&!a.hasOwnProperty(c)||b.call(d,a[c],c,a);else if(M(a)||za(a)){var f="object"!==typeof a;c=0;for(e=a.length;c<e;c++)(f||c in a)&&b.call(d,a[c],c,a)}else if(a.forEach&&a.forEach!==q)a.forEach(b,d,a);else if(oc(a))for(c in a)b.call(d,a[c],c,a);else if("function"===typeof a.hasOwnProperty)for(c in a)a.hasOwnProperty(c)&&
b.call(d,a[c],c,a);else for(c in a)va.call(a,c)&&b.call(d,a[c],c,a);return a}function pc(a,b,d){for(var c=Object.keys(a).sort(),e=0;e<c.length;e++)b.call(d,a[c[e]],c[e]);return c}function qc(a){return function(b,d){a(d,b)}}function Wd(){return++qb}function Ob(a,b,d){for(var c=a.$$hashKey,e=0,f=b.length;e<f;++e){var g=b[e];if(J(g)||D(g))for(var h=Object.keys(g),k=0,l=h.length;k<l;k++){var m=h[k],n=g[m];d&&J(n)?fa(n)?a[m]=new Date(n.valueOf()):Za(n)?a[m]=new RegExp(n):n.nodeName?a[m]=n.cloneNode(!0):
Pb(n)?a[m]=n.clone():(J(a[m])||(a[m]=M(n)?[]:{}),Ob(a[m],[n],!0)):a[m]=n}}c?a.$$hashKey=c:delete a.$$hashKey;return a}function S(a){return Ob(a,Aa.call(arguments,1),!1)}function Xd(a){return Ob(a,Aa.call(arguments,1),!0)}function Y(a){return parseInt(a,10)}function Qb(a,b){return S(Object.create(a),b)}function E(){}function $a(a){return a}function da(a){return function(){return a}}function rc(a){return D(a.toString)&&a.toString!==ka}function z(a){return"undefined"===typeof a}function A(a){return"undefined"!==
typeof a}function J(a){return null!==a&&"object"===typeof a}function oc(a){return null!==a&&"object"===typeof a&&!sc(a)}function y(a){return"string"===typeof a}function R(a){return"number"===typeof a}function fa(a){return"[object Date]"===ka.call(a)}function D(a){return"function"===typeof a}function Za(a){return"[object RegExp]"===ka.call(a)}function Ya(a){return a&&a.window===a}function ab(a){return a&&a.$evalAsync&&a.$watch}function Oa(a){return"boolean"===typeof a}function Yd(a){return a&&R(a.length)&&
Zd.test(ka.call(a))}function Pb(a){return!(!a||!(a.nodeName||a.prop&&a.attr&&a.find))}function $d(a){var b={};a=a.split(",");var d;for(d=0;d<a.length;d++)b[a[d]]=!0;return b}function oa(a){return N(a.nodeName||a[0]&&a[0].nodeName)}function bb(a,b){var d=a.indexOf(b);0<=d&&a.splice(d,1);return d}function pa(a,b){function d(a,b){var d=b.$$hashKey,e;if(M(a)){e=0;for(var f=a.length;e<f;e++)b.push(c(a[e]))}else if(oc(a))for(e in a)b[e]=c(a[e]);else if(a&&"function"===typeof a.hasOwnProperty)for(e in a)a.hasOwnProperty(e)&&
(b[e]=c(a[e]));else for(e in a)va.call(a,e)&&(b[e]=c(a[e]));d?b.$$hashKey=d:delete b.$$hashKey;return b}function c(a){if(!J(a))return a;var b=f.indexOf(a);if(-1!==b)return g[b];if(Ya(a)||ab(a))throw Ba("cpws");var b=!1,c=e(a);c===u&&(c=M(a)?[]:Object.create(sc(a)),b=!0);f.push(a);g.push(c);return b?d(a,c):c}function e(a){switch(ka.call(a)){case "[object Int8Array]":case "[object Int16Array]":case "[object Int32Array]":case "[object Float32Array]":case "[object Float64Array]":case "[object Uint8Array]":case "[object Uint8ClampedArray]":case "[object Uint16Array]":case "[object Uint32Array]":return new a.constructor(c(a.buffer));
case "[object ArrayBuffer]":if(!a.slice){var b=new ArrayBuffer(a.byteLength);(new Uint8Array(b)).set(new Uint8Array(a));return b}return a.slice(0);case "[object Boolean]":case "[object Number]":case "[object String]":case "[object Date]":return new a.constructor(a.valueOf());case "[object RegExp]":return b=new RegExp(a.source,a.toString().match(/[^\/]*$/)[0]),b.lastIndex=a.lastIndex,b;case "[object Blob]":return new a.constructor([a],{type:a.type})}if(D(a.cloneNode))return a.cloneNode(!0)}var f=[],
g=[];if(b){if(Yd(b)||"[object ArrayBuffer]"===ka.call(b))throw Ba("cpta");if(a===b)throw Ba("cpi");M(b)?b.length=0:q(b,function(a,c){"$$hashKey"!==c&&delete b[c]});f.push(a);g.push(b);return d(a,b)}return c(a)}function ia(a,b){if(M(a)){b=b||[];for(var d=0,c=a.length;d<c;d++)b[d]=a[d]}else if(J(a))for(d in b=b||{},a)if("$"!==d.charAt(0)||"$"!==d.charAt(1))b[d]=a[d];return b||a}function na(a,b){if(a===b)return!0;if(null===a||null===b)return!1;if(a!==a&&b!==b)return!0;var d=typeof a,c;if(d==typeof b&&
"object"==d)if(M(a)){if(!M(b))return!1;if((d=a.length)==b.length){for(c=0;c<d;c++)if(!na(a[c],b[c]))return!1;return!0}}else{if(fa(a))return fa(b)?na(a.getTime(),b.getTime()):!1;if(Za(a))return Za(b)?a.toString()==b.toString():!1;if(ab(a)||ab(b)||Ya(a)||Ya(b)||M(b)||fa(b)||Za(b))return!1;d=V();for(c in a)if("$"!==c.charAt(0)&&!D(a[c])){if(!na(a[c],b[c]))return!1;d[c]=!0}for(c in b)if(!(c in d)&&"$"!==c.charAt(0)&&A(b[c])&&!D(b[c]))return!1;return!0}return!1}function cb(a,b,d){return a.concat(Aa.call(b,
d))}function tc(a,b){var d=2<arguments.length?Aa.call(arguments,2):[];return!D(b)||b instanceof RegExp?b:d.length?function(){return arguments.length?b.apply(a,cb(d,arguments,0)):b.apply(a,d)}:function(){return arguments.length?b.apply(a,arguments):b.call(a)}}function ae(a,b){var d=b;"string"===typeof a&&"$"===a.charAt(0)&&"$"===a.charAt(1)?d=u:Ya(b)?d="$WINDOW":b&&P===b?d="$DOCUMENT":ab(b)&&(d="$SCOPE");return d}function db(a,b){if(z(a))return u;R(b)||(b=b?2:null);return JSON.stringify(a,ae,b)}function uc(a){return y(a)?
JSON.parse(a):a}function vc(a,b){a=a.replace(be,"");var d=Date.parse("Jan 01, 1970 00:00:00 "+a)/6E4;return isNaN(d)?b:d}function Rb(a,b,d){d=d?-1:1;var c=a.getTimezoneOffset();b=vc(b,c);d*=b-c;a=new Date(a.getTime());a.setMinutes(a.getMinutes()+d);return a}function wa(a){a=H(a).clone();try{a.empty()}catch(b){}var d=H("<div>").append(a).html();try{return a[0].nodeType===Pa?N(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+N(b)})}catch(c){return N(d)}}function wc(a){try{return decodeURIComponent(a)}catch(b){}}
function xc(a){var b={};q((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=wc(e),A(e)&&(f=A(f)?wc(f):!0,va.call(b,e)?M(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Sb(a){var b=[];q(a,function(a,c){M(a)?q(a,function(a){b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))}):b.push(ja(c,!0)+(!0===a?"":"="+ja(a,!0)))});return b.length?b.join("&"):""}function rb(a){return ja(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,
"=").replace(/%2B/gi,"+")}function ja(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function ce(a,b){var d,c,e=Qa.length;for(c=0;c<e;++c)if(d=Qa[c]+b,y(d=a.getAttribute(d)))return d;return null}function de(a,b){var d,c,e={};q(Qa,function(b){b+="app";!d&&a.hasAttribute&&a.hasAttribute(b)&&(d=a,c=a.getAttribute(b))});q(Qa,function(b){b+="app";var e;!d&&(e=a.querySelector("["+b.replace(":",
"\\:")+"]"))&&(d=e,c=e.getAttribute(b))});d&&(e.strictDi=null!==ce(d,"strict-di"),b(d,c?[c]:[],e))}function yc(a,b,d){J(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=H(a);if(a.injector()){var c=a[0]===P?"document":wa(a);throw Ba("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope",
"$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;T&&e.test(T.name)&&(d.debugInfoEnabled=!0,T.name=T.name.replace(e,""));if(T&&!f.test(T.name))return c();T.name=T.name.replace(f,"");ea.resumeBootstrap=function(a){q(a,function(a){b.push(a)});return c()};D(ea.resumeDeferredBootstrap)&&ea.resumeDeferredBootstrap()}function ee(){T.name="NG_ENABLE_DEBUG_INFO!"+T.name;T.location.reload()}
function fe(a){a=ea.element(a).injector();if(!a)throw Ba("test");return a.get("$$testability")}function zc(a,b){b=b||"_";return a.replace(ge,function(a,c){return(c?b:"")+a.toLowerCase()})}function he(){var a;if(!Ac){var b=sb();($=z(b)?T.jQuery:b?T[b]:u)&&$.fn.on?(H=$,S($.fn,{scope:Ra.scope,isolateScope:Ra.isolateScope,controller:Ra.controller,injector:Ra.injector,inheritedData:Ra.inheritedData}),a=$.cleanData,$.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=$._data(f,"events"))&&c.$destroy&&
$(f).triggerHandler("$destroy");a(b)}):H=U;ea.element=H;Ac=!0}}function tb(a,b,d){if(!a)throw Ba("areq",b||"?",d||"required");return a}function Sa(a,b,d){d&&M(a)&&(a=a[a.length-1]);tb(D(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ta(a,b){if("hasOwnProperty"===a)throw Ba("badname",b);}function Bc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g<f;g++)c=b[g],a&&(a=(e=a)[c]);return!d&&D(a)?tc(e,a):a}function ub(a){for(var b=
a[0],d=a[a.length-1],c,e=1;b!==d&&(b=b.nextSibling);e++)if(c||a[e]!==b)c||(c=H(Aa.call(a,0,e))),c.push(b);return c||a}function V(){return Object.create(null)}function ie(a){function b(a,b,c){return a[b]||(a[b]=c())}var d=O("$injector"),c=O("ng");a=b(a,"angular",Object);a.$$minErr=a.$$minErr||O;return b(a,"module",function(){var a={};return function(f,g,h){if("hasOwnProperty"===f)throw c("badname","module");g&&a.hasOwnProperty(f)&&(a[f]=null);return b(a,f,function(){function a(b,d,e,f){f||(f=c);return function(){f[e||
"push"]([b,d,arguments]);return L}}function b(a,d){return function(b,e){e&&D(e)&&(e.$$moduleName=f);c.push([a,d,arguments]);return L}}if(!g)throw d("nomod",f);var c=[],e=[],p=[],F=a("$injector","invoke","push",e),L={_invokeQueue:c,_configBlocks:e,_runBlocks:p,requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator"),animation:b("$animateProvider",
"register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),component:b("$compileProvider","component"),config:F,run:function(a){p.push(a);return this}};h&&F(h);return L})}})}function je(a){S(a,{bootstrap:yc,copy:pa,extend:S,merge:Xd,equals:na,element:H,forEach:q,injector:eb,noop:E,bind:tc,toJson:db,fromJson:uc,identity:$a,isUndefined:z,isDefined:A,isString:y,isFunction:D,isObject:J,isNumber:R,isElement:Pb,isArray:M,
version:ke,isDate:fa,lowercase:N,uppercase:vb,callbacks:{counter:0},getTestability:fe,$$minErr:O,$$csp:Ga,reloadWithDebugInfo:ee});Tb=ie(T);Tb("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:le});a.provider("$compile",Cc).directive({a:me,input:Dc,textarea:Dc,form:ne,script:oe,select:pe,style:qe,option:re,ngBind:se,ngBindHtml:te,ngBindTemplate:ue,ngClass:ve,ngClassEven:we,ngClassOdd:xe,ngCloak:ye,ngController:ze,ngForm:Ae,ngHide:Be,ngIf:Ce,ngInclude:De,ngInit:Ee,ngNonBindable:Fe,
ngPluralize:Ge,ngRepeat:He,ngShow:Ie,ngStyle:Je,ngSwitch:Ke,ngSwitchWhen:Le,ngSwitchDefault:Me,ngOptions:Ne,ngTransclude:Oe,ngModel:Pe,ngList:Qe,ngChange:Re,pattern:Ec,ngPattern:Ec,required:Fc,ngRequired:Fc,minlength:Gc,ngMinlength:Gc,maxlength:Hc,ngMaxlength:Hc,ngValue:Se,ngModelOptions:Te}).directive({ngInclude:Ue}).directive(wb).directive(Ic);a.provider({$anchorScroll:Ve,$animate:We,$animateCss:Xe,$$animateJs:Ye,$$animateQueue:Ze,$$AnimateRunner:$e,$$animateAsyncRun:af,$browser:bf,$cacheFactory:cf,
$controller:df,$document:ef,$exceptionHandler:ff,$filter:Jc,$$forceReflow:gf,$interpolate:hf,$interval:jf,$http:kf,$httpParamSerializer:lf,$httpParamSerializerJQLike:mf,$httpBackend:nf,$xhrFactory:of,$location:pf,$log:qf,$parse:rf,$rootScope:sf,$q:tf,$$q:uf,$sce:vf,$sceDelegate:wf,$sniffer:xf,$templateCache:yf,$templateRequest:zf,$$testability:Af,$timeout:Bf,$window:Cf,$$rAF:Df,$$jqLite:Ef,$$HashMap:Ff,$$cookieReader:Gf})}])}function fb(a){return a.replace(Hf,function(a,d,c,e){return e?c.toUpperCase():
c}).replace(If,"Moz$1")}function Kc(a){a=a.nodeType;return 1===a||!a||9===a}function Lc(a,b){var d,c,e=b.createDocumentFragment(),f=[];if(Ub.test(a)){d=d||e.appendChild(b.createElement("div"));c=(Jf.exec(a)||["",""])[1].toLowerCase();c=ha[c]||ha._default;d.innerHTML=c[1]+a.replace(Kf,"<$1></$2>")+c[2];for(c=c[0];c--;)d=d.lastChild;f=cb(f,d.childNodes);d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function Mc(a,
b){var d=a.parentNode;d&&d.replaceChild(b,a);b.appendChild(a)}function U(a){if(a instanceof U)return a;var b;y(a)&&(a=W(a),b=!0);if(!(this instanceof U)){if(b&&"<"!=a.charAt(0))throw Vb("nosel");return new U(a)}if(b){b=P;var d;a=(d=Lf.exec(a))?[b.createElement(d[1])]:(d=Lc(a,b))?d.childNodes:[]}Nc(this,a)}function Wb(a){return a.cloneNode(!0)}function xb(a,b){b||gb(a);if(a.querySelectorAll)for(var d=a.querySelectorAll("*"),c=0,e=d.length;c<e;c++)gb(d[c])}function Oc(a,b,d,c){if(A(c))throw Vb("offargs");
var e=(c=yb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];A(d)&&bb(c||[],d);A(d)&&c&&0<c.length||(a.removeEventListener(b,f,!1),delete e[b])};q(b.split(" "),function(a){g(a);zb[a]&&g(zb[a])})}else for(b in e)"$destroy"!==b&&a.removeEventListener(b,f,!1),delete e[b]}function gb(a,b){var d=a.ng339,c=d&&hb[d];c&&(b?delete c.data[b]:(c.handle&&(c.events.$destroy&&c.handle({},"$destroy"),Oc(a)),delete hb[d],a.ng339=u))}function yb(a,b){var d=a.ng339,d=d&&hb[d];b&&!d&&(a.ng339=d=++Mf,
d=hb[d]={events:{},data:{},handle:u});return d}function Xb(a,b,d){if(Kc(a)){var c=A(d),e=!c&&b&&!J(b),f=!b;a=(a=yb(a,!e))&&a.data;if(c)a[b]=d;else{if(f)return a;if(e)return a&&a[b];S(a,b)}}}function Ab(a,b){return a.getAttribute?-1<(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+b+" "):!1}function Bb(a,b){b&&a.setAttribute&&q(b.split(" "),function(b){a.setAttribute("class",W((" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+W(b)+" "," ")))})}function Cb(a,
b){if(b&&a.setAttribute){var d=(" "+(a.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");q(b.split(" "),function(a){a=W(a);-1===d.indexOf(" "+a+" ")&&(d+=a+" ")});a.setAttribute("class",W(d))}}function Nc(a,b){if(b)if(b.nodeType)a[a.length++]=b;else{var d=b.length;if("number"===typeof d&&b.window!==b){if(d)for(var c=0;c<d;c++)a[a.length++]=b[c]}else a[a.length++]=b}}function Pc(a,b){return Db(a,"$"+(b||"ngController")+"Controller")}function Db(a,b,d){9==a.nodeType&&(a=a.documentElement);for(b=
M(b)?b:[b];a;){for(var c=0,e=b.length;c<e;c++)if(A(d=H.data(a,b[c])))return d;a=a.parentNode||11===a.nodeType&&a.host}}function Qc(a){for(xb(a,!0);a.firstChild;)a.removeChild(a.firstChild)}function Yb(a,b){b||xb(a);var d=a.parentNode;d&&d.removeChild(a)}function Nf(a,b){b=b||T;if("complete"===b.document.readyState)b.setTimeout(a);else H(b).on("load",a)}function Rc(a,b){var d=Eb[b.toLowerCase()];return d&&Sc[oa(a)]&&d}function Of(a,b){var d=function(c,d){c.isDefaultPrevented=function(){return c.defaultPrevented};
var f=b[d||c.type],g=f?f.length:0;if(g){if(z(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};var k=f.specialHandlerWrapper||Pf;1<g&&(f=ia(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||k(a,c,f[l])}};d.elem=a;return d}function Pf(a,b,d){d.call(a,b)}function Qf(a,b,
d){var c=b.relatedTarget;c&&(c===a||Rf.call(a,c))||d.call(a,b)}function Ef(){this.$get=function(){return S(U,{hasClass:function(a,b){a.attr&&(a=a[0]);return Ab(a,b)},addClass:function(a,b){a.attr&&(a=a[0]);return Cb(a,b)},removeClass:function(a,b){a.attr&&(a=a[0]);return Bb(a,b)}})}}function Ha(a,b){var d=a&&a.$$hashKey;if(d)return"function"===typeof d&&(d=a.$$hashKey()),d;d=typeof a;return d="function"==d||"object"==d&&null!==a?a.$$hashKey=d+":"+(b||Wd)():d+":"+a}function Ua(a,b){if(b){var d=0;this.nextUid=
function(){return++d}}q(a,this.put,this)}function Tc(a){a=a.toString().replace(Sf,"");return a.match(Tf)||a.match(Uf)}function Vf(a){return(a=Tc(a))?"function("+(a[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function eb(a,b){function d(a){return function(b,c){if(J(b))q(b,qc(a));else return a(b,c)}}function c(a,b){Ta(a,"service");if(D(b)||M(b))b=p.instantiate(b);if(!b.$get)throw Ia("pget",a);return n[a+"Provider"]=b}function e(a,b){return function(){var c=x.invoke(b,this);if(z(c))throw Ia("undef",a);
return c}}function f(a,b,d){return c(a,{$get:!1!==d?e(a,b):b})}function g(a){tb(z(a)||M(a),"modulesToLoad","not an array");var b=[],c;q(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=p.get(e[0]);f[e[1]].apply(f,e[2])}}if(!m.get(a)){m.put(a,!0);try{y(a)?(c=Tb(a),b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):D(a)?b.push(p.invoke(a)):M(a)?b.push(p.invoke(a)):Sa(a,"module")}catch(e){throw M(a)&&(a=a[a.length-1]),e.message&&e.stack&&
-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ia("modulerr",a,e.stack||e.message||e);}}});return b}function h(a,c){function d(b,e){if(a.hasOwnProperty(b)){if(a[b]===k)throw Ia("cdep",b+" <- "+l.join(" <- "));return a[b]}try{return l.unshift(b),a[b]=k,a[b]=c(b,e)}catch(f){throw a[b]===k&&delete a[b],f;}finally{l.shift()}}function e(a,c,f){var g=[];a=eb.$$annotate(a,b,f);for(var h=0,k=a.length;h<k;h++){var l=a[h];if("string"!==typeof l)throw Ia("itkn",l);g.push(c&&c.hasOwnProperty(l)?c[l]:
d(l,f))}return g}return{invoke:function(a,b,c,d){"string"===typeof c&&(d=c,c=null);c=e(a,c,d);M(a)&&(a=a[a.length-1]);d=11>=Da?!1:"function"===typeof a&&/^(?:class\s|constructor\()/.test(Function.prototype.toString.call(a));return d?(c.unshift(null),new (Function.prototype.bind.apply(a,c))):a.apply(b,c)},instantiate:function(a,b,c){var d=M(a)?a[a.length-1]:a;a=e(a,b,c);a.unshift(null);return new (Function.prototype.bind.apply(d,a))},get:d,annotate:eb.$$annotate,has:function(b){return n.hasOwnProperty(b+
"Provider")||a.hasOwnProperty(b)}}}b=!0===b;var k={},l=[],m=new Ua([],!0),n={$provide:{provider:d(c),factory:d(f),service:d(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:d(function(a,b){return f(a,da(b),!1)}),constant:d(function(a,b){Ta(a,"constant");n[a]=b;F[a]=b}),decorator:function(a,b){var c=p.get(a+"Provider"),d=c.$get;c.$get=function(){var a=x.invoke(d,c);return x.invoke(b,null,{$delegate:a})}}}},p=n.$injector=h(n,function(a,b){ea.isString(b)&&l.push(b);
throw Ia("unpr",l.join(" <- "));}),F={},L=h(F,function(a,b){var c=p.get(a+"Provider",b);return x.invoke(c.$get,c,u,a)}),x=L;n.$injectorProvider={$get:da(L)};var r=g(a),x=L.get("$injector");x.strictDi=b;q(r,function(a){a&&x.invoke(a)});return x}function Ve(){var a=!0;this.disableAutoScrolling=function(){a=!1};this.$get=["$window","$location","$rootScope",function(b,d,c){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===oa(a))return b=a,!0});return b}function f(a){if(a){a.scrollIntoView();
var c;c=g.yOffset;D(c)?c=c():Pb(c)?(c=c[0],c="fixed"!==b.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):R(c)||(c=0);c&&(a=a.getBoundingClientRect().top,b.scrollBy(0,a-c))}else b.scrollTo(0,0)}function g(a){a=y(a)?a:d.hash();var b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=b.document;a&&c.$watch(function(){return d.hash()},function(a,b){a===b&&""===a||Nf(function(){c.$evalAsync(g)})});return g}]}function ib(a,b){if(!a&&!b)return"";
if(!a)return b;if(!b)return a;M(a)&&(a=a.join(" "));M(b)&&(b=b.join(" "));return a+" "+b}function Wf(a){y(a)&&(a=a.split(" "));var b=V();q(a,function(a){a.length&&(b[a]=!0)});return b}function Ja(a){return J(a)?a:{}}function Xf(a,b,d,c){function e(a){try{a.apply(null,Aa.call(arguments,1))}finally{if(L--,0===L)for(;x.length;)try{x.pop()()}catch(b){d.error(b)}}}function f(){t=null;g();h()}function g(){r=G();r=z(r)?null:r;na(r,I)&&(r=I);I=r}function h(){if(v!==k.url()||w!==r)v=k.url(),w=r,q(C,function(a){a(k.url(),
r)})}var k=this,l=a.location,m=a.history,n=a.setTimeout,p=a.clearTimeout,F={};k.isMock=!1;var L=0,x=[];k.$$completeOutstandingRequest=e;k.$$incOutstandingRequestCount=function(){L++};k.notifyWhenNoOutstandingRequests=function(a){0===L?a():x.push(a)};var r,w,v=l.href,Q=b.find("base"),t=null,G=c.history?function(){try{return m.state}catch(a){}}:E;g();w=r;k.url=function(b,d,e){z(e)&&(e=null);l!==a.location&&(l=a.location);m!==a.history&&(m=a.history);if(b){var f=w===e;if(v===b&&(!c.history||f))return k;
var h=v&&Ka(v)===Ka(b);v=b;w=e;if(!c.history||h&&f){if(!h||t)t=b;d?l.replace(b):h?(d=l,e=b.indexOf("#"),e=-1===e?"":b.substr(e),d.hash=e):l.href=b;l.href!==b&&(t=b)}else m[d?"replaceState":"pushState"](e,"",b),g(),w=r;return k}return t||l.href.replace(/%27/g,"'")};k.state=function(){return r};var C=[],K=!1,I=null;k.onUrlChange=function(b){if(!K){if(c.history)H(a).on("popstate",f);H(a).on("hashchange",f);K=!0}C.push(b);return b};k.$$applicationDestroyed=function(){H(a).off("hashchange popstate",f)};
k.$$checkUrlChange=h;k.baseHref=function(){var a=Q.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};k.defer=function(a,b){var c;L++;c=n(function(){delete F[c];e(a)},b||0);F[c]=!0;return c};k.defer.cancel=function(a){return F[a]?(delete F[a],p(a),e(E),!0):!1}}function bf(){this.$get=["$window","$log","$sniffer","$document",function(a,b,d,c){return new Xf(a,c,b,d)}]}function cf(){this.$get=function(){function a(a,c){function e(a){a!=n&&(p?p==a&&(p=a.n):p=a,f(a.n,a.p),f(a,n),n=a,n.n=
null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(a in b)throw O("$cacheFactory")("iid",a);var g=0,h=S({},c,{id:a}),k=V(),l=c&&c.capacity||Number.MAX_VALUE,m=V(),n=null,p=null;return b[a]={put:function(a,b){if(!z(b)){if(l<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}a in k||g++;k[a]=b;g>l&&this.remove(p.key);return b}},get:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return k[a]},remove:function(a){if(l<Number.MAX_VALUE){var b=m[a];if(!b)return;b==n&&(n=b.p);b==p&&(p=
b.n);f(b.n,b.p);delete m[a]}a in k&&(delete k[a],g--)},removeAll:function(){k=V();g=0;m=V();n=p=null},destroy:function(){m=h=k=null;delete b[a]},info:function(){return S({},h,{size:g})}}}var b={};a.info=function(){var a={};q(b,function(b,e){a[e]=b.info()});return a};a.get=function(a){return b[a]};return a}}function yf(){this.$get=["$cacheFactory",function(a){return a("templates")}]}function Cc(a,b){function d(a,b,c){var d=/^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/,e={};q(a,function(a,f){if(a in m)e[f]=
m[a];else{var g=a.match(d);if(!g)throw ga("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f};g[4]&&(m[a]=e[f])}});return e}function c(a){var b=a.charAt(0);if(!b||b!==N(b))throw ga("baddir",a);if(a!==a.trim())throw ga("baddir",a);}var e={},f=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,g=/(([\w\-]+)(?:\:([^;]+))?;?)/,h=$d("ngSrc,ngSrcset,src,srcset"),k=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,l=/^(on[a-z]+|formaction)$/,
m=V();this.directive=function L(b,d){Ta(b,"directive");y(b)?(c(b),tb(d,"directiveFactory"),e.hasOwnProperty(b)||(e[b]=[],a.factory(b+"Directive",["$injector","$exceptionHandler",function(a,c){var d=[];q(e[b],function(e,f){try{var g=a.invoke(e);D(g)?g={compile:da(g)}:!g.compile&&g.link&&(g.compile=da(g.link));g.priority=g.priority||0;g.index=f;g.name=g.name||b;g.require=g.require||g.controller&&g.name;g.restrict=g.restrict||"EA";g.$$moduleName=e.$$moduleName;d.push(g)}catch(h){c(h)}});return d}])),
e[b].push(d)):q(b,qc(L));return this};this.component=function(a,b){function c(a){function e(b){return D(b)||M(b)?function(c,d){return a.invoke(b,this,{$element:c,$attrs:d})}:b}var f=b.template||b.templateUrl?b.template:"";return{controller:d,controllerAs:Uc(b.controller)||b.controllerAs||"$ctrl",template:e(f),templateUrl:e(b.templateUrl),transclude:b.transclude,scope:{},bindToController:b.bindings||{},restrict:"E",require:b.require}}var d=b.controller||E;q(b,function(a,b){"$"===b.charAt(0)&&(c[b]=
a,d[b]=a)});c.$inject=["$injector"];return this.directive(a,c)};this.aHrefSanitizationWhitelist=function(a){return A(a)?(b.aHrefSanitizationWhitelist(a),this):b.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(a){return A(a)?(b.imgSrcSanitizationWhitelist(a),this):b.imgSrcSanitizationWhitelist()};var n=!0;this.debugInfoEnabled=function(a){return A(a)?(n=a,this):n};var p=10;this.onChangesTtl=function(a){return arguments.length?(p=a,this):p};this.$get=["$injector","$interpolate",
"$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$sce","$animate","$$sanitizeUri",function(a,b,c,m,v,Q,t,G,C,K){function I(){try{if(!--pa)throw $=u,ga("infchng",p);t.$apply(function(){for(var a=0,b=$.length;a<b;++a)$[a]();$=u})}finally{pa++}}function qa(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a}function Ca(a,b,c){la.innerHTML="<span "+b+">";b=la.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);
d.value=c;a.attributes.setNamedItem(d)}function B(a,b){try{a.addClass(b)}catch(c){}}function ba(a,b,c,d,e){a instanceof H||(a=H(a));for(var f=/\S+/,g=0,h=a.length;g<h;g++){var k=a[g];k.nodeType===Pa&&k.nodeValue.match(f)&&Mc(k,a[g]=P.createElement("span"))}var l=xa(a,b,a,c,d,e);ba.$$addScopeClass(a);var m=null;return function(b,c,d){tb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var f=d.parentBoundTranscludeFn,g=d.transcludeControllers;d=d.futureParentElement;f&&f.$$boundTransclude&&
(f=f.$$boundTransclude);m||(m=(d=d&&d[0])?"foreignobject"!==oa(d)&&ka.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==m?H(ca(m,H("<div>").append(a).html())):c?Ra.clone.call(a):a;if(g)for(var h in g)d.data("$"+h+"Controller",g[h].instance);ba.$$addScopeInfo(d,b);c&&c(d,b);l&&l(b,d,d,f);return d}}function xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,n,p,G;if(r)for(G=Array(c.length),m=0;m<h.length;m+=3)f=h[m],G[f]=c[f];else G=c;m=0;for(n=h.length;m<n;)k=G[h[m++]],c=h[m++],f=h[m++],c?(c.scope?
(l=a.$new(),ba.$$addScopeInfo(H(k),l)):l=a,p=c.transcludeOnThisElement?s(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?s(a,b):null,c(f,l,k,d,p)):f&&f(a,k.childNodes,u,e)}for(var h=[],k,l,m,n,r,p=0;p<a.length;p++){k=new qa;l=A(a[p],[],k,0===p?d:u,e);(f=l.length?ra(l,a[p],k,b,c,null,[],[],f):null)&&f.scope&&ba.$$addScopeClass(k.$$element);k=f&&f.terminal||!(m=a[p].childNodes)||!m.length?null:xa(m,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)h.push(p,f,k),
n=!0,r=r||f;f=null}return n?g:null}function s(a,b,c){function d(e,f,g,h,k){e||(e=a.$new(!1,k),e.$$transcluded=!0);return b(e,f,{parentBoundTranscludeFn:c,transcludeControllers:g,futureParentElement:h})}var e=d.$$slots=V(),f;for(f in b.$$slots)e[f]=b.$$slots[f]?s(a,b.$$slots[f],c):null;return d}function A(a,b,c,d,e){var h=c.$attr,k;switch(a.nodeType){case 1:Fa(b,ya(oa(a)),"E",d,e);for(var l,m,n,r=a.attributes,p=0,G=r&&r.length;p<G;p++){var v=!1,C=!1;l=r[p];k=l.name;m=W(l.value);l=ya(k);if(n=za.test(l))k=
k.replace(Vc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});(l=l.match(Ba))&&R(l[1])&&(v=k,C=k.substr(0,k.length-5)+"end",k=k.substr(0,k.length-6));l=ya(k.toLowerCase());h[l]=k;if(n||!c.hasOwnProperty(l))c[l]=m,Rc(a,l)&&(c[l]=!0);fa(a,b,m,l,n);Fa(b,l,"A",d,e,v,C)}a=a.className;J(a)&&(a=a.animVal);if(y(a)&&""!==a)for(;k=g.exec(a);)l=ya(k[2]),Fa(b,l,"C",d,e)&&(c[l]=W(k[3])),a=a.substr(k.index+k[0].length);break;case Pa:if(11===Da)for(;a.parentNode&&a.nextSibling&&a.nextSibling.nodeType===
Pa;)a.nodeValue+=a.nextSibling.nodeValue,a.parentNode.removeChild(a.nextSibling);Y(b,a.nodeValue);break;case 8:try{if(k=f.exec(a.nodeValue))l=ya(k[1]),Fa(b,l,"M",d,e)&&(c[l]=W(k[2]))}catch(w){}}b.sort(Z);return b}function Wc(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ga("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return H(d)}function O(a,b,c){return function(d,e,f,g,h){e=Wc(e[0],
b,c);return a(d,e,f,g,h)}}function Zb(a,b,c,d,e,f){var g;return a?ba(b,c,d,e,f):function(){g||(g=ba(b,c,d,e,f),b=c=f=null);return g.apply(this,arguments)}}function ra(a,b,d,e,f,g,h,k,l){function m(a,b,c,d){if(a){c&&(a=O(a,c,d));a.require=B.require;a.directiveName=L;if(C===B||B.$$isolateScope)a=ia(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=O(b,c,d));b.require=B.require;b.directiveName=L;if(C===B||B.$$isolateScope)b=ia(b,{isolateScope:!0});k.push(b)}}function n(a,c,e,f,g){function l(a,b,c,d){var e;
ab(a)||(d=c,c=b,b=a,a=u);Ca&&(e=K);c||(c=Ca?t.parent():t);if(d){var f=g.$$slots[d];if(f)return f(a,b,e,c,s);if(z(f))throw ga("noslot",d,wa(t));}else return g(a,b,e,c,s)}var m,r,p,B,I,K,x,t;b===e?(f=d,t=d.$$element):(t=H(e),f=new qa(t,d));I=c;C?B=c.$new(!0):G&&(I=c.$parent);g&&(x=l,x.$$boundTransclude=g,x.isSlotFilled=function(a){return!!g.$$slots[a]});v&&(K=T(t,f,x,v,B,c,C));C&&(ba.$$addScopeInfo(t,B,!0,!(w&&(w===C||w===C.$$originalDirective))),ba.$$addScopeClass(t,!0),B.$$isolateBindings=C.$$isolateBindings,
(p=ha(c,f,B,B.$$isolateBindings,C))&&B.$on("$destroy",p));for(r in K){p=v[r];var Va=K[r],Q=p.$$bindings.bindToController;Va.identifier&&Q&&(m=ha(I,f,Va.instance,Q,p));var L=Va();L!==Va.instance&&(Va.instance=L,t.data("$"+p.name+"Controller",L),m&&m(),m=ha(I,f,Va.instance,Q,p))}q(v,function(a,b){var c=a.require;a.bindToController&&!M(c)&&J(c)&&S(K[b].instance,jb(b,c,t,K))});q(K,function(a){var b=a.instance;D(b.$onInit)&&b.$onInit();D(b.$onDestroy)&&I.$on("$destroy",function(){b.$onDestroy()})});m=
0;for(r=h.length;m<r;m++)p=h[m],ja(p,p.isolateScope?B:c,t,f,p.require&&jb(p.directiveName,p.require,t,K),x);var s=c;C&&(C.template||null===C.templateUrl)&&(s=B);a&&a(s,e.childNodes,u,g);for(m=k.length-1;0<=m;m--)p=k[m],ja(p,p.isolateScope?B:c,t,f,p.require&&jb(p.directiveName,p.require,t,K),x);q(K,function(a){a=a.instance;D(a.$postLink)&&a.$postLink()})}l=l||{};for(var p=-Number.MAX_VALUE,G=l.newScopeDirective,v=l.controllerDirectives,C=l.newIsolateScopeDirective,w=l.templateDirective,I=l.nonTlbTranscludeDirective,
K=!1,x=!1,Ca=l.hasElementTranscludeDirective,t=d.$$element=H(b),B,L,Q,s=e,xa,Ea=!1,E=!1,y,ra=0,N=a.length;ra<N;ra++){B=a[ra];var R=B.$$start,Fa=B.$$end;R&&(t=Wc(b,R,Fa));Q=u;if(p>B.priority)break;if(y=B.scope)B.templateUrl||(J(y)?(X("new/isolated scope",C||G,B,t),C=B):X("new/isolated scope",C,B,t)),G=G||B;L=B.name;if(!Ea&&(B.replace&&(B.templateUrl||B.template)||B.transclude&&!B.$$tlb)){for(y=ra+1;Ea=a[y++];)if(Ea.transclude&&!Ea.$$tlb||Ea.replace&&(Ea.templateUrl||Ea.template)){E=!0;break}Ea=!0}!B.templateUrl&&
B.controller&&(y=B.controller,v=v||V(),X("'"+L+"' controller",v[L],B,t),v[L]=B);if(y=B.transclude)if(K=!0,B.$$tlb||(X("transclusion",I,B,t),I=B),"element"==y)Ca=!0,p=B.priority,Q=t,t=d.$$element=H(ba.$$createComment(L,d[L])),b=t[0],da(f,Aa.call(Q,0),b),Q[0].$$parentNode=Q[0].parentNode,s=Zb(E,Q,e,p,g&&g.name,{nonTlbTranscludeDirective:I});else{var P=V();Q=H(Wb(b)).contents();if(J(y)){Q=[];var Z=V(),Y=V();q(y,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Z[a]=b;P[b]=null;Y[b]=c});q(t.contents(),
function(a){var b=Z[ya(oa(a))];b?(Y[b]=!0,P[b]=P[b]||[],P[b].push(a)):Q.push(a)});q(Y,function(a,b){if(!a)throw ga("reqslot",b);});for(var $ in P)P[$]&&(P[$]=Zb(E,P[$],e))}t.empty();s=Zb(E,Q,e,u,u,{needsNewScope:B.$$isolateScope||B.$$newScope});s.$$slots=P}if(B.template)if(x=!0,X("template",w,B,t),w=B,y=D(B.template)?B.template(t,d):B.template,y=ua(y),B.replace){g=B;Q=Ub.test(y)?Xc(ca(B.templateNamespace,W(y))):[];b=Q[0];if(1!=Q.length||1!==b.nodeType)throw ga("tplrt",L,"");da(f,t,b);N={$attr:{}};
y=A(b,[],N);var ea=a.splice(ra+1,a.length-(ra+1));(C||G)&&Yc(y,C,G);a=a.concat(y).concat(ea);U(d,N);N=a.length}else t.html(y);if(B.templateUrl)x=!0,X("template",w,B,t),w=B,B.replace&&(g=B),n=aa(a.splice(ra,a.length-ra),t,d,f,K&&s,h,k,{controllerDirectives:v,newScopeDirective:G!==B&&G,newIsolateScopeDirective:C,templateDirective:w,nonTlbTranscludeDirective:I}),N=a.length;else if(B.compile)try{xa=B.compile(t,d,s),D(xa)?m(null,xa,R,Fa):xa&&m(xa.pre,xa.post,R,Fa)}catch(fa){c(fa,wa(t))}B.terminal&&(n.terminal=
!0,p=Math.max(p,B.priority))}n.scope=G&&!0===G.scope;n.transcludeOnThisElement=K;n.templateOnThisElement=x;n.transclude=s;l.hasElementTranscludeDirective=Ca;return n}function jb(a,b,c,d){var e;if(y(b)){var f=b.match(k);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw ga("ctreq",b,a);}else if(M(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=jb(a,b[g],c,d);else J(b)&&(e=
{},q(b,function(b,f){e[f]=jb(a,b,c,d)}));return e||null}function T(a,b,c,d,e,f,g){var h=V(),k;for(k in d){var l=d[k],m={$scope:l===g||l.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},n=l.controller;"@"==n&&(n=b[l.name]);m=Q(n,m,!0,l.controllerAs);h[l.name]=m;a.data("$"+l.name+"Controller",m.instance)}return h}function Yc(a,b,c){for(var d=0,e=a.length;d<e;d++)a[d]=Qb(a[d],{$$isolateScope:b,$$newScope:c})}function Fa(b,f,g,h,k,l,m){if(f===k)return null;k=null;if(e.hasOwnProperty(f)){var n;f=
a.get(f+"Directive");for(var p=0,G=f.length;p<G;p++)try{if(n=f[p],(z(h)||h>n.priority)&&-1!=n.restrict.indexOf(g)){l&&(n=Qb(n,{$$start:l,$$end:m}));if(!n.$$bindings){var v=n,C=n,w=n.name,B={isolateScope:null,bindToController:null};J(C.scope)&&(!0===C.bindToController?(B.bindToController=d(C.scope,w,!0),B.isolateScope={}):B.isolateScope=d(C.scope,w,!1));J(C.bindToController)&&(B.bindToController=d(C.bindToController,w,!0));if(J(B.bindToController)){var I=C.controller,K=C.controllerAs;if(!I)throw ga("noctrl",
w);if(!Uc(I,K))throw ga("noident",w);}var x=v.$$bindings=B;J(x.isolateScope)&&(n.$$isolateBindings=x.isolateScope)}b.push(n);k=n}}catch(t){c(t)}}return k}function R(b){if(e.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,f=c.length;d<f;d++)if(b=c[d],b.multiElement)return!0;return!1}function U(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,f){"class"==f?(B(e,b),a["class"]=(a["class"]?
a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function aa(a,b,c,d,e,f,g,h){var k=[],l,n,p=b[0],r=a.shift(),G=Qb(r,{templateUrl:null,transclude:null,replace:null,$$originalDirective:r}),v=D(r.templateUrl)?r.templateUrl(b,c):r.templateUrl,C=r.templateNamespace;b.empty();m(v).then(function(m){var w,I;m=ua(m);if(r.replace){m=Ub.test(m)?Xc(ca(C,W(m))):[];w=m[0];if(1!=m.length||1!==
w.nodeType)throw ga("tplrt",r.name,v);m={$attr:{}};da(d,b,w);var K=A(w,[],m);J(r.scope)&&Yc(K,!0);a=K.concat(a);U(c,m)}else w=p,b.html(m);a.unshift(G);l=ra(a,w,c,e,b,r,f,g,h);q(d,function(a,c){a==w&&(d[c]=b[0])});for(n=xa(b[0].childNodes,e);k.length;){m=k.shift();I=k.shift();var x=k.shift(),t=k.shift(),K=b[0];if(!m.$$destroyed){if(I!==p){var qa=I.className;h.hasElementTranscludeDirective&&r.replace||(K=Wb(w));da(x,H(I),K);B(H(K),qa)}I=l.transcludeOnThisElement?s(m,l.transclude,t):t;l(n,m,K,d,I)}}k=
null});return function(a,b,c,d,e){a=e;b.$$destroyed||(k?k.push(b,c,d,a):(l.transcludeOnThisElement&&(a=s(b,l.transclude,e)),l(n,b,c,d,a)))}}function Z(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function X(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw ga("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,wa(d));}function Y(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;
b&&ba.$$addBindingClass(a);return function(a,c){var e=c.parent();b||ba.$$addBindingClass(e);ba.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function ca(a,b){a=N(a||"html");switch(a){case "svg":case "math":var c=P.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function ea(a,b){if("srcdoc"==b)return G.HTML;var c=oa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return G.RESOURCE_URL}
function fa(a,c,d,e,f){var g=ea(a,e);f=h[e]||f;var k=b(d,!0,g,f);if(k){if("multiple"===e&&"select"===oa(a))throw ga("selmulti",wa(a));c.push({priority:100,compile:function(){return{pre:function(a,c,h){c=h.$$observers||(h.$$observers=V());if(l.test(e))throw ga("nodomevents");var m=h[e];m!==d&&(k=m&&b(m,!0,g,f),d=m);k&&(h[e]=k(a),(c[e]||(c[e]=[])).$$inter=!0,(h.$$observers&&h.$$observers[e].$$scope||a).$watch(k,function(a,b){"class"===e&&a!=b?h.$updateClass(a,b):h.$set(e,a)}))}}}})}}function da(a,b,
c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=P.createDocumentFragment();for(g=0;g<e;g++)a.appendChild(b[g]);H.hasData(d)&&(H.data(c,H.data(d)),H(d).off("$destroy"));H.cleanData(a.querySelectorAll("*"));for(g=1;g<e;g++)delete b[g];b[0]=c;b.length=1}function ia(a,b){return S(function(){return a.apply(null,arguments)},
a,b)}function ja(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,wa(d))}}function ha(a,c,d,e,f){function g(b,c,e){D(d.$onChanges)&&c!==e&&($||(a.$$postDigest(I),$=[]),l||(l={},$.push(h)),l[b]&&(e=l[b].previousValue),l[b]={previousValue:e,currentValue:c})}function h(){d.$onChanges(l);l=u}var k=[],l;q(e,function(e,h){var l=e.attrName,m=e.optional,n,r,p,G;switch(e.mode){case "@":m||va.call(c,l)||(d[h]=c[l]=void 0);c.$observe(l,function(a){y(a)&&(g(h,a,d[h]),d[h]=a)});c.$$observers[l].$$scope=a;n=c[l];y(n)?
d[h]=b(n)(a):Oa(n)&&(d[h]=n);break;case "=":if(!va.call(c,l)){if(m)break;c[l]=void 0}if(m&&!c[l])break;r=v(c[l]);G=r.literal?na:function(a,b){return a===b||a!==a&&b!==b};p=r.assign||function(){n=d[h]=r(a);throw ga("nonassign",c[l],l,f.name);};n=d[h]=r(a);m=function(b){G(b,d[h])||(G(b,n)?p(a,b=d[h]):d[h]=b);return n=b};m.$stateful=!0;m=e.collection?a.$watchCollection(c[l],m):a.$watch(v(c[l],m),null,r.literal);k.push(m);break;case "<":if(!va.call(c,l)){if(m)break;c[l]=void 0}if(m&&!c[l])break;r=v(c[l]);
d[h]=r(a);m=a.$watch(r,function(a){g(h,a,d[h]);d[h]=a},r.literal);k.push(m);break;case "&":r=c.hasOwnProperty(l)?v(c[l]):E;if(r===E&&m)break;d[h]=function(b){return r(a,b)}}});return k.length&&function(){for(var a=0,b=k.length;a<b;++a)k[a]()}}var ma=/^\w/,la=P.createElement("div"),pa=p,$;qa.prototype={$normalize:ya,$addClass:function(a){a&&0<a.length&&C.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&C.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=Zc(a,b);c&&
c.length&&C.addClass(this.$$element,c);(c=Zc(b,a))&&c.length&&C.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=Rc(this.$$element[0],a),g=$c[a],h=a;f?(this.$$element.prop(a,b),e=f):g&&(this[g]=b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=zc(a,"-"));f=oa(this.$$element);if("a"===f&&("href"===a||"xlinkHref"===a)||"img"===f&&"src"===a)this[a]=b=K(b,"src"===a);else if("img"===f&&"srcset"===a){for(var f="",g=W(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(g)?
k:/(,)/,g=g.split(k),k=Math.floor(g.length/2),l=0;l<k;l++)var m=2*l,f=f+K(W(g[m]),!0),f=f+(" "+W(g[m+1]));g=W(g[2*l]).split(/\s/);f+=K(W(g[0]),!0);2===g.length&&(f+=" "+W(g[1]));this[a]=b=f}!1!==d&&(null===b||z(b)?this.$$element.removeAttr(e):ma.test(e)?this.$$element.attr(e,b):Ca(this.$$element[0],e,b));(a=this.$$observers)&&q(a[h],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=V()),e=d[a]||(d[a]=[]);e.push(b);t.$evalAsync(function(){e.$$inter||
!c.hasOwnProperty(a)||z(c[a])||b(c[a])});return function(){bb(e,b)}}};var sa=b.startSymbol(),ta=b.endSymbol(),ua="{{"==sa&&"}}"==ta?$a:function(a){return a.replace(/\{\{/g,sa).replace(/}}/g,ta)},za=/^ngAttr[A-Z]/,Ba=/^(.+)Start$/;ba.$$addBindingInfo=n?function(a,b){var c=a.data("$binding")||[];M(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:E;ba.$$addBindingClass=n?function(a){B(a,"ng-binding")}:E;ba.$$addScopeInfo=n?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",
b)}:E;ba.$$addScopeClass=n?function(a,b){B(a,b?"ng-isolate-scope":"ng-scope")}:E;ba.$$createComment=function(a,b){var c="";n&&(c=" "+(a||"")+": "+(b||"")+" ");return P.createComment(c)};return ba}]}function ya(a){return fb(a.replace(Vc,""))}function Zc(a,b){var d="",c=a.split(/\s+/),e=b.split(/\s+/),f=0;a:for(;f<c.length;f++){for(var g=c[f],h=0;h<e.length;h++)if(g==e[h])continue a;d+=(0<d.length?" ":"")+g}return d}function Xc(a){a=H(a);var b=a.length;if(1>=b)return a;for(;b--;)8===a[b].nodeType&&
Yf.call(a,b,1);return a}function Uc(a,b){if(b&&y(b))return b;if(y(a)){var d=ad.exec(a);if(d)return d[3]}}function df(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Ta(b,"controller");J(b)?S(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!J(a.$scope))throw O("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&y(k)&&(n=k);if(y(f)){k=f.match(ad);
if(!k)throw Zf("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Bc(g.$scope,m,!0)||(b?Bc(c,m,!0):u);Sa(f,m,!0)}if(h)return h=(M(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n,l,m||f.name),S(function(){var a=d.invoke(f,l,g,m);a!==l&&(J(a)||D(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function ef(){this.$get=["$window",function(a){return H(a.document)}]}function ff(){this.$get=["$log",function(a){return function(b,
d){a.error.apply(a,arguments)}}]}function $b(a){return J(a)?fa(a)?a.toISOString():db(a):a}function lf(){this.$get=function(){return function(a){if(!a)return"";var b=[];pc(a,function(a,c){null===a||z(a)||(M(a)?q(a,function(a){b.push(ja(c)+"="+ja($b(a)))}):b.push(ja(c)+"="+ja($b(a))))});return b.join("&")}}}function mf(){this.$get=function(){return function(a){function b(a,e,f){null===a||z(a)||(M(a)?q(a,function(a,c){b(a,e+"["+(J(a)?c:"")+"]")}):J(a)&&!fa(a)?pc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?
"":"]"))}):d.push(ja(e)+"="+ja($b(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function ac(a,b){if(y(a)){var d=a.replace($f,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(bd))||(c=(c=d.match(ag))&&bg[c[0]].test(d));c&&(a=uc(d))}}return a}function cd(a){var b=V(),d;y(a)?q(a.split("\n"),function(a){d=a.indexOf(":");var e=N(W(a.substr(0,d)));a=W(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):J(a)&&q(a,function(a,d){var f=N(d),g=W(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}
function dd(a){var b;return function(d){b||(b=cd(a));return d?(d=b[N(d)],void 0===d&&(d=null),d):b}}function ed(a,b,d,c){if(D(c))return c(a,b,d);q(c,function(c){a=c(a,b,d)});return a}function kf(){var a=this.defaults={transformResponse:[ac],transformRequest:[function(a){return J(a)&&"[object File]"!==ka.call(a)&&"[object Blob]"!==ka.call(a)&&"[object FormData]"!==ka.call(a)?db(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ia(bc),put:ia(bc),patch:ia(bc)},xsrfCookieName:"XSRF-TOKEN",
xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer"},b=!1;this.useApplyAsync=function(a){return A(a)?(b=!!a,this):b};var d=!0;this.useLegacyPromiseExtensions=function(a){return A(a)?(d=!!a,this):d};var c=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector",function(e,f,g,h,k,l){function m(b){function c(a){var b=S({},a);b.data=ed(a.data,a.headers,a.status,f.transformResponse);a=a.status;return 200<=a&&300>a?b:k.reject(b)}function e(a,
b){var c,d={};q(a,function(a,e){D(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}if(!J(b))throw O("$http")("badreq",b);if(!y(b.url))throw O("$http")("badreq",b.url);var f=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer},b);f.headers=function(b){var c=a.headers,d=S({},b.headers),f,g,h,c=S({},c.common,c[N(b.method)]);a:for(f in c){g=N(f);for(h in d)if(N(h)===g)continue a;d[f]=c[f]}return e(d,ia(b))}(b);f.method=vb(f.method);
f.paramSerializer=y(f.paramSerializer)?l.get(f.paramSerializer):f.paramSerializer;var g=[function(b){var d=b.headers,e=ed(b.data,dd(d),u,b.transformRequest);z(e)&&q(d,function(a,b){"content-type"===N(b)&&delete d[b]});z(b.withCredentials)&&!z(a.withCredentials)&&(b.withCredentials=a.withCredentials);return n(b,e).then(c,c)},u],h=k.when(f);for(q(L,function(a){(a.request||a.requestError)&&g.unshift(a.request,a.requestError);(a.response||a.responseError)&&g.push(a.response,a.responseError)});g.length;){b=
g.shift();var m=g.shift(),h=h.then(b,m)}d?(h.success=function(a){Sa(a,"fn");h.then(function(b){a(b.data,b.status,b.headers,f)});return h},h.error=function(a){Sa(a,"fn");h.then(null,function(b){a(b.data,b.status,b.headers,f)});return h}):(h.success=fd("success"),h.error=fd("error"));return h}function n(c,d){function g(a,c,d,e){function f(){l(c,a,d,e)}K&&(200<=a&&300>a?K.put(L,[a,c,cd(d),e]):K.remove(L));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function l(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?G.resolve:
G.reject)({data:a,status:b,headers:dd(d),config:c,statusText:e})}function n(a){l(a.data,a.status,ia(a.headers()),a.statusText)}function t(){var a=m.pendingRequests.indexOf(c);-1!==a&&m.pendingRequests.splice(a,1)}var G=k.defer(),C=G.promise,K,I,qa=c.headers,L=p(c.url,c.paramSerializer(c.params));m.pendingRequests.push(c);C.then(t,t);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(K=J(c.cache)?c.cache:J(a.cache)?a.cache:F);K&&(I=K.get(L),A(I)?I&&D(I.then)?I.then(n,n):M(I)?
l(I[1],I[0],ia(I[2]),I[3]):l(I,200,{},"OK"):K.put(L,C));z(I)&&((I=gd(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:u)&&(qa[c.xsrfHeaderName||a.xsrfHeaderName]=I),e(c.method,L,d,g,qa,c.timeout,c.withCredentials,c.responseType));return C}function p(a,b){0<b.length&&(a+=(-1==a.indexOf("?")?"?":"&")+b);return a}var F=g("$http");a.paramSerializer=y(a.paramSerializer)?l.get(a.paramSerializer):a.paramSerializer;var L=[];q(c,function(a){L.unshift(y(a)?l.get(a):l.invoke(a))});m.pendingRequests=[];(function(a){q(arguments,
function(a){m[a]=function(b,c){return m(S({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){m[a]=function(b,c,d){return m(S({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");m.defaults=a;return m}]}function of(){this.$get=function(){return function(){return new T.XMLHttpRequest}}}function nf(){this.$get=["$browser","$window","$document","$xhrFactory",function(a,b,d,c){return cg(a,c,a.defer,b.angular.callbacks,d[0])}]}function cg(a,b,d,
c,e){function f(a,b,d){var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f.async=!0;m=function(a){f.removeEventListener("load",m,!1);f.removeEventListener("error",m,!1);e.body.removeChild(f);f=null;var g=-1,F="unknown";a&&("load"!==a.type||c[b].called||(a={type:"error"}),F=a.type,g="error"===a.type?404:200);d&&d(g,F)};f.addEventListener("load",m,!1);f.addEventListener("error",m,!1);e.body.appendChild(f);return m}return function(e,h,k,l,m,n,p,F){function L(){w&&w();v&&v.abort()}
function x(b,c,e,f,g){A(t)&&d.cancel(t);w=v=null;b(c,e,f,g);a.$$completeOutstandingRequest(E)}a.$$incOutstandingRequestCount();h=h||a.url();if("jsonp"==N(e)){var r="_"+(c.counter++).toString(36);c[r]=function(a){c[r].data=a;c[r].called=!0};var w=f(h.replace("JSON_CALLBACK","angular.callbacks."+r),r,function(a,b){x(l,a,c[r].data,"",b);c[r]=E})}else{var v=b(e,h);v.open(e,h,!0);q(m,function(a,b){A(a)&&v.setRequestHeader(b,a)});v.onload=function(){var a=v.statusText||"",b="response"in v?v.response:v.responseText,
c=1223===v.status?204:v.status;0===c&&(c=b?200:"file"==sa(h).protocol?404:0);x(l,c,b,v.getAllResponseHeaders(),a)};e=function(){x(l,-1,null,null,"")};v.onerror=e;v.onabort=e;p&&(v.withCredentials=!0);if(F)try{v.responseType=F}catch(Q){if("json"!==F)throw Q;}v.send(z(k)?null:k)}if(0<n)var t=d(L,n);else n&&D(n.then)&&n.then(L)}}function hf(){var a="{{",b="}}";this.startSymbol=function(b){return b?(a=b,this):a};this.endSymbol=function(a){return a?(b=a,this):b};this.$get=["$parse","$exceptionHandler",
"$sce",function(d,c,e){function f(a){return"\\\\\\"+a}function g(c){return c.replace(n,a).replace(p,b)}function h(a,b,c,d){var e;return e=a.$watch(function(a){e();return d(a)},b,c)}function k(f,k,n,r){function p(a){try{var b=a;a=n?e.getTrusted(n,b):e.valueOf(b);var d;if(r&&!A(a))d=a;else if(null==a)d="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=db(a)}d=a}return d}catch(g){c(La.interr(f,g))}}if(!f.length||-1===f.indexOf(a)){var v;k||(k=g(f),v=da(k),v.exp=f,v.expressions=
[],v.$$watchDelegate=h);return v}r=!!r;var q,t,G=0,C=[],K=[];v=f.length;for(var I=[],qa=[];G<v;)if(-1!=(q=f.indexOf(a,G))&&-1!=(t=f.indexOf(b,q+l)))G!==q&&I.push(g(f.substring(G,q))),G=f.substring(q+l,t),C.push(G),K.push(d(G,p)),G=t+m,qa.push(I.length),I.push("");else{G!==v&&I.push(g(f.substring(G)));break}n&&1<I.length&&La.throwNoconcat(f);if(!k||C.length){var Ca=function(a){for(var b=0,c=C.length;b<c;b++){if(r&&z(a[b]))return;I[qa[b]]=a[b]}return I.join("")};return S(function(a){var b=0,d=C.length,
e=Array(d);try{for(;b<d;b++)e[b]=K[b](a);return Ca(e)}catch(g){c(La.interr(f,g))}},{exp:f,expressions:C,$$watchDelegate:function(a,b){var c;return a.$watchGroup(K,function(d,e){var f=Ca(d);D(b)&&b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=a.length,m=b.length,n=new RegExp(a.replace(/./g,f),"g"),p=new RegExp(b.replace(/./g,f),"g");k.startSymbol=function(){return a};k.endSymbol=function(){return b};return k}]}function jf(){this.$get=["$rootScope","$window","$q","$$q","$browser",function(a,b,d,c,e){function f(f,
k,l,m){function n(){p?f.apply(null,F):f(r)}var p=4<arguments.length,F=p?Aa.call(arguments,4):[],q=b.setInterval,x=b.clearInterval,r=0,w=A(m)&&!m,v=(w?c:d).defer(),Q=v.promise;l=A(l)?l:0;Q.$$intervalId=q(function(){w?e.defer(n):a.$evalAsync(n);v.notify(r++);0<l&&r>=l&&(v.resolve(r),x(Q.$$intervalId),delete g[Q.$$intervalId]);w||a.$apply()},k);g[Q.$$intervalId]=v;return Q}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),
delete g[a.$$intervalId],!0):!1};return f}]}function cc(a){a=a.split("/");for(var b=a.length;b--;)a[b]=rb(a[b]);return a.join("/")}function hd(a,b){var d=sa(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Y(d.port)||dg[d.protocol]||null}function id(a,b){var d="/"!==a.charAt(0);d&&(a="/"+a);var c=sa(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=xc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!=b.$$path.charAt(0)&&(b.$$path=
"/"+b.$$path)}function la(a,b){if(0===b.indexOf(a))return b.substr(a.length)}function Ka(a){var b=a.indexOf("#");return-1==b?a:a.substr(0,b)}function kb(a){return a.replace(/(#.+)|#$/,"$1")}function dc(a,b,d){this.$$html5=!0;d=d||"";hd(a,this);this.$$parse=function(a){var d=la(b,a);if(!y(d))throw Fb("ipthprfx",a,b);id(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Sb(this.$$search),d=this.$$hash?"#"+rb(this.$$hash):"";this.$$url=cc(this.$$path)+(a?"?"+a:"")+
d;this.$$absUrl=b+this.$$url.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;A(f=la(a,c))?(g=f,g=A(f=la(d,f))?b+(la("/",f)||f):a+g):A(f=la(b,c))?g=b+f:b==c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function ec(a,b,d){hd(a,this);this.$$parse=function(c){var e=la(a,c)||la(b,c),f;z(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",z(e)&&(a=c,this.replace())):(f=la(d,e),z(f)&&(f=e));id(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&
(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Sb(this.$$search),e=this.$$hash?"#"+rb(this.$$hash):"";this.$$url=cc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"")};this.$$parseLinkUrl=function(b,d){return Ka(a)==Ka(b)?(this.$$parse(b),!0):!1}}function jd(a,b,d){this.$$html5=!0;ec.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a==Ka(c)?
f=c:(g=la(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Sb(this.$$search),e=this.$$hash?"#"+rb(this.$$hash):"";this.$$url=cc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url}}function Gb(a){return function(){return this[a]}}function kd(a,b){return function(d){if(z(d))return this[a];this[a]=b(d);this.$$compose();return this}}function pf(){var a="",b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return A(b)?(a=b,this):
a};this.html5Mode=function(a){return Oa(a)?(b.enabled=a,this):J(a)?(Oa(a.enabled)&&(b.enabled=a.enabled),Oa(a.requireBase)&&(b.requireBase=a.requireBase),Oa(a.rewriteLinks)&&(b.rewriteLinks=a.rewriteLinks),this):b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state=f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,
b)}var l,m;m=c.baseHref();var n=c.url(),p;if(b.enabled){if(!m&&b.requireBase)throw Fb("nobase");p=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?dc:jd}else p=Ka(n),m=ec;var F=p.substr(0,Ka(p).lastIndexOf("/")+1);l=new m(p,F,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var q=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(b.rewriteLinks&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!=a.which&&2!=a.button){for(var e=H(a.target);"a"!==oa(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;
var h=e.prop("href"),k=e.attr("href")||e.attr("xlink:href");J(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=sa(h.animVal).href);q.test(h)||!h||e.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(h,k)||(a.preventDefault(),l.absUrl()!=c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}});kb(l.absUrl())!=kb(n)&&c.url(l.absUrl(),!0);var x=!0;c.onUrlChange(function(a,b){z(la(F,a))?g.location.href=a:(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=kb(a);l.$$parse(a);
l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(x=!1,k(c,e)))}),d.$$phase||d.$digest())});d.$watch(function(){var a=kb(c.url()),b=kb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(x||m)x=!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:
l.$$state),k(a,f)))});l.$$replace=!1});return l}]}function qf(){var a=!0,b=this;this.debugEnabled=function(b){return A(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;a=!1;try{a=!!e.apply}catch(k){}return a?function(){var a=[];q(arguments,function(b){a.push(c(b))});
return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Wa(a,b){if("__defineGetter__"===a||"__defineSetter__"===a||"__lookupGetter__"===a||"__lookupSetter__"===a||"__proto__"===a)throw ca("isecfld",b);return a}function eg(a){return a+""}function ta(a,b){if(a){if(a.constructor===a)throw ca("isecfn",b);if(a.window===a)throw ca("isecwindow",b);if(a.children&&
(a.nodeName||a.prop&&a.attr&&a.find))throw ca("isecdom",b);if(a===Object)throw ca("isecobj",b);}return a}function ld(a,b){if(a){if(a.constructor===a)throw ca("isecfn",b);if(a===fg||a===gg||a===hg)throw ca("isecff",b);}}function Hb(a,b){if(a&&(a===(0).constructor||a===(!1).constructor||a==="".constructor||a==={}.constructor||a===[].constructor||a===Function.constructor))throw ca("isecaf",b);}function ig(a,b){return"undefined"!==typeof a?a:b}function md(a,b){return"undefined"===typeof a?b:"undefined"===
typeof b?a:a+b}function aa(a,b){var d,c;switch(a.type){case s.Program:d=!0;q(a.body,function(a){aa(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:aa(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:aa(a.left,b);aa(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:aa(a.left,
b);aa(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:aa(a.test,b);aa(a.alternate,b);aa(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:aa(a.object,b);a.computed&&aa(a.property,b);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=
a.filter?!b(a.callee.name).$stateful:!1;c=[];q(a.arguments,function(a){aa(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=a.filter&&!b(a.callee.name).$stateful?c:[a];break;case s.AssignmentExpression:aa(a.left,b);aa(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];q(a.elements,function(a){aa(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=c;break;case s.ObjectExpression:d=
!0;c=[];q(a.properties,function(a){aa(a.value,b);d=d&&a.value.constant;a.value.constant||c.push.apply(c,a.value.toWatch)});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function nd(a){if(1==a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:u}}function od(a){return a.type===s.Identifier||a.type===s.MemberExpression}function pd(a){if(1===a.body.length&&od(a.body[0].expression))return{type:s.AssignmentExpression,
left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function qd(a){return 0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression)}function rd(a,b){this.astBuilder=a;this.$filter=b}function sd(a,b){this.astBuilder=a;this.$filter=b}function Ib(a){return"constructor"==a}function fc(a){return D(a.valueOf)?a.valueOf():jg.call(a)}function rf(){var a=V(),b=V(),d={"true":!0,
"false":!1,"null":null,undefined:u};this.addLiteral=function(a,b){d[a]=b};this.$get=["$filter",function(c){function e(d,e,g){var p,t,G;g=g||x;switch(typeof d){case "string":G=d=d.trim();var C=g?b:a;p=C[G];if(!p){":"===d.charAt(0)&&":"===d.charAt(1)&&(t=!0,d=d.substring(2));p=g?L:F;var K=new gc(p);p=(new hc(K,c,p)).parse(d);p.constant?p.$$watchDelegate=m:t?p.$$watchDelegate=p.literal?l:k:p.inputs&&(p.$$watchDelegate=h);g&&(p=f(p));C[G]=p}return n(p,e);case "function":return n(d,e);default:return n(E,
e)}}function f(a){function b(c,d,e,f){var g=x;x=!0;try{return a(c,d,e,f)}finally{x=g}}if(!a)return a;b.$$watchDelegate=a.$$watchDelegate;b.assign=f(a.assign);b.constant=a.constant;b.literal=a.literal;for(var c=0;a.inputs&&c<a.inputs.length;++c)a.inputs[c]=f(a.inputs[c]);b.inputs=a.inputs;return b}function g(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=fc(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function h(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=
f(a);g(b,k)||(h=d(a,u,u,[b]),k=b&&fc(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n<p;n++)l[n]=g,m[n]=null;return a.$watch(function(a){for(var b=!1,c=0,e=f.length;c<e;c++){var k=f[c](a);if(b||(b=!g(k,l[c])))m[c]=k,l[c]=k&&fc(k)}b&&(h=d(a,u,u,m));return h},b,c,e)}function k(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;D(b)&&b.apply(this,arguments);A(a)&&d.$$postDigest(function(){A(f)&&e()})},c)}function l(a,b,c,d){function e(a){var b=!0;q(a,function(a){A(a)||
(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,c,d){g=a;D(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function m(a,b,c,d){var e;return e=a.$watch(function(a){e();return d(a)},b,c)}function n(a,b){if(!b)return a;var c=a.$$watchDelegate,d=!1,c=c!==l&&c!==k?function(c,e,f,g){f=d&&g?g[0]:a(c,e,f,g);return b(f,c,e)}:function(c,d,e,f){e=a(c,d,e,f);c=b(e,c,d);return A(e)?c:e};a.$$watchDelegate&&a.$$watchDelegate!==h?c.$$watchDelegate=a.$$watchDelegate:
b.$stateful||(c.$$watchDelegate=h,d=!a.inputs,c.inputs=a.inputs?a.inputs:[a]);return c}var p=Ga().noUnsafeEval,F={csp:p,expensiveChecks:!1,literals:pa(d)},L={csp:p,expensiveChecks:!0,literals:pa(d)},x=!1;e.$$runningExpensiveChecks=function(){return x};return e}]}function tf(){this.$get=["$rootScope","$exceptionHandler",function(a,b){return td(function(b){a.$evalAsync(b)},b)}]}function uf(){this.$get=["$browser","$exceptionHandler",function(a,b){return td(function(b){a.defer(b)},b)}]}function td(a,
b){function d(){this.$$state={status:0}}function c(a,b){return function(c){b.call(a,c)}}function e(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,a(function(){var a,d,e;e=c.pending;c.processScheduled=!1;c.pending=u;for(var f=0,g=e.length;f<g;++f){d=e[f][0];a=e[f][c.status];try{D(a)?d.resolve(a(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),b(h)}}}))}function f(){this.promise=new d}var g=O("$q",TypeError);S(d.prototype,{then:function(a,b,c){if(z(a)&&z(b)&&
z(c))return this;var d=new f;this.$$state.pending=this.$$state.pending||[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&e(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return k(b,!0,a)},function(b){return k(b,!1,a)},b)}});S(f.prototype,{resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(g("qcycle",a)):this.$$resolve(a))},$$resolve:function(a){function d(a){k||(k=!0,h.$$resolve(a))}
function f(a){k||(k=!0,h.$$reject(a))}var g,h=this,k=!1;try{if(J(a)||D(a))g=a&&a.then;D(g)?(this.promise.$$state.status=-1,g.call(a,d,f,c(this,this.notify))):(this.promise.$$state.value=a,this.promise.$$state.status=1,e(this.promise.$$state))}catch(l){f(l),b(l)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;e(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&
d&&d.length&&a(function(){for(var a,e,f=0,g=d.length;f<g;f++){e=d[f][0];a=d[f][3];try{e.notify(D(a)?a(c):c)}catch(h){b(h)}}})}});var h=function(a,b){var c=new f;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{D(c)&&(d=c())}catch(e){return h(e,!1)}return d&&D(d.then)?d.then(function(){return h(a,b)},function(a){return h(a,!1)}):h(a,b)},l=function(a,b,c,d){var e=new f;e.resolve(a);return e.promise.then(b,c,d)},m=function(a){if(!D(a))throw g("norslvr",a);var b=new f;a(function(a){b.resolve(a)},
function(a){b.reject(a)});return b.promise};m.prototype=d.prototype;m.defer=function(){var a=new f;a.resolve=c(a,a.resolve);a.reject=c(a,a.reject);a.notify=c(a,a.notify);return a};m.reject=function(a){var b=new f;b.reject(a);return b.promise};m.when=l;m.resolve=l;m.all=function(a){var b=new f,c=0,d=M(a)?[]:{};q(a,function(a,e){c++;l(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return m}function Df(){this.$get=
["$window","$timeout",function(a,b){var d=a.requestAnimationFrame||a.webkitRequestAnimationFrame,c=a.cancelAnimationFrame||a.webkitCancelAnimationFrame||a.webkitCancelRequestAnimationFrame,e=!!d,f=e?function(a){var b=d(a);return function(){c(b)}}:function(a){var c=b(a,16.66,!1);return function(){b.cancel(c)}};f.supported=e;return f}]}function sf(){function a(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=
0;this.$id=++qb;this.$$ChildScope=null}b.prototype=a;return b}var b=10,d=O("$rootScope"),c=null,e=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$exceptionHandler","$parse","$browser",function(f,g,h){function k(a){a.currentScope.$$destroyed=!0}function l(a){9===Da&&(a.$$childHead&&l(a.$$childHead),a.$$nextSibling&&l(a.$$nextSibling));a.$parent=a.$$nextSibling=a.$$prevSibling=a.$$childHead=a.$$childTail=a.$root=a.$$watchers=null}function m(){this.$id=++qb;this.$$phase=
this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function n(a){if(w.$$phase)throw d("inprog",w.$$phase);w.$$phase=a}function p(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function F(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function s(){}function x(){for(;t.length;)try{t.shift()()}catch(a){f(a)}e=
null}function r(){null===e&&(e=h.defer(function(){w.$apply(x)}))}m.prototype={constructor:m,$new:function(b,c){var d;c=c||this;b?(d=new m,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=a(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(b||c!=this)&&d.$on("$destroy",k);return d},$watch:function(a,b,d,e){var f=g(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,d,f,
a);var h=this,k=h.$$watchers,l={fn:b,last:s,get:f,exp:e||a,eq:!!d};c=null;D(b)||(l.fn=E);k||(k=h.$$watchers=[]);k.unshift(l);p(this,1);return function(){0<=bb(k,l)&&p(h,-1);c=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});q(a,function(a,
b){var k=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!z(e)){if(J(e))if(za(e))for(f!==n&&(f=n,v=f.length=0,l++),a=e.length,v!==a&&(l++,f.length=v=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==p&&(f=p={},v=0,l++);a=0;for(b in e)va.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(v++,f[b]=g,l++));if(v>
a)for(b in l++,f)va.call(e,b)||(v--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1<b.length,l=0,m=g(a,c),n=[],p={},r=!0,v=0;return this.$watch(m,function(){r?(r=!1,b(e,e,d)):b(e,h,d);if(k)if(J(e))if(za(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else for(a in h={},e)va.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var a,g,k,l,m,p,r,q,t=b,F,A=[],z,y;n("$digest");h.$$checkUrlChange();this===w&&null!==e&&(h.defer.cancel(e),x());c=null;do{q=!1;
for(F=this;v.length;){try{y=v.shift(),y.scope.$eval(y.expression,y.locals)}catch(E){f(E)}c=null}a:do{if(p=F.$$watchers)for(r=p.length;r--;)try{if(a=p[r])if(m=a.get,(g=m(F))!==(k=a.last)&&!(a.eq?na(g,k):"number"===typeof g&&"number"===typeof k&&isNaN(g)&&isNaN(k)))q=!0,c=a,a.last=a.eq?pa(g,null):g,l=a.fn,l(g,k===s?g:k,F),5>t&&(z=4-t,A[z]||(A[z]=[]),A[z].push({msg:D(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){q=!1;break a}}catch(H){f(H)}if(!(p=F.$$watchersCount&&
F.$$childHead||F!==this&&F.$$nextSibling))for(;F!==this&&!(p=F.$$nextSibling);)F=F.$parent}while(F=p);if((q||v.length)&&!t--)throw w.$$phase=null,d("infdig",b,A);}while(q||v.length);for(w.$$phase=null;u.length;)try{u.shift()()}catch(J){f(J)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===w&&h.$$applicationDestroyed();p(this,-this.$$watchersCount);for(var b in this.$$listenerCount)F(this,this.$$listenerCount[b],b);a&&a.$$childHead==
this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=E;this.$on=this.$watch=this.$watchGroup=function(){return E};this.$$listeners={};this.$$nextSibling=null;l(this)}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){w.$$phase||
v.length||h.defer(function(){v.length&&w.$digest()});v.push({scope:this,expression:g(a),locals:b})},$$postDigest:function(a){u.push(a)},$apply:function(a){try{n("$apply");try{return this.$eval(a)}finally{w.$$phase=null}}catch(b){f(b)}finally{try{w.$digest()}catch(c){throw f(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&t.push(b);a=g(a);r()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=
0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,F(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,g=!1,h={name:a,targetScope:e,stopPropagation:function(){g=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=cb([h],arguments,1),l,m;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(m=d.length;l<m;l++)if(d[l])try{d[l].apply(null,k)}catch(n){f(n)}else d.splice(l,1),l--,m--;if(g)return h.currentScope=null,h;e=e.$parent}while(e);
h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var g=cb([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,g)}catch(l){f(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=
null;return e}};var w=new m,v=w.$$asyncQueue=[],u=w.$$postDigestQueue=[],t=w.$$applyAsyncQueue=[];return w}]}function le(){var a=/^\s*(https?|ftp|mailto|tel|file):/,b=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(b){return A(b)?(a=b,this):a};this.imgSrcSanitizationWhitelist=function(a){return A(a)?(b=a,this):b};this.$get=function(){return function(d,c){var e=c?b:a,f;f=sa(d).href;return""===f||f.match(e)?d:"unsafe:"+f}}}function kg(a){if("self"===a)return a;
if(y(a)){if(-1<a.indexOf("***"))throw ua("iwcard",a);a=ud(a).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+a+"$")}if(Za(a))return new RegExp("^"+a.source+"$");throw ua("imatcher");}function vd(a){var b=[];A(a)&&q(a,function(a){b.push(kg(a))});return b}function wf(){this.SCE_CONTEXTS=ma;var a=["self"],b=[];this.resourceUrlWhitelist=function(b){arguments.length&&(a=vd(b));return a};this.resourceUrlBlacklist=function(a){arguments.length&&(b=vd(a));return b};this.$get=["$injector",
function(d){function c(a,b){return"self"===a?gd(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var f=function(a){throw ua("unsafe");};d.has("$sanitize")&&(f=d.get("$sanitize"));var g=e(),h={};h[ma.HTML]=e(g);h[ma.CSS]=e(g);h[ma.URL]=e(g);h[ma.JS]=e(g);h[ma.RESOURCE_URL]=
e(h[ma.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw ua("icontext",a,b);if(null===b||z(b)||""===b)return b;if("string"!==typeof b)throw ua("itype",a);return new c(b)},getTrusted:function(d,e){if(null===e||z(e)||""===e)return e;var g=h.hasOwnProperty(d)?h[d]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(d===ma.RESOURCE_URL){var g=sa(e.toString()),n,p,q=!1;n=0;for(p=a.length;n<p;n++)if(c(a[n],g)){q=!0;break}if(q)for(n=0,p=b.length;n<p;n++)if(c(b[n],
g)){q=!1;break}if(q)return e;throw ua("insecurl",e.toString());}if(d===ma.HTML)return f(e);throw ua("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function vf(){var a=!0;this.enabled=function(b){arguments.length&&(a=!!b);return a};this.$get=["$parse","$sceDelegate",function(b,d){if(a&&8>Da)throw ua("iequirks");var c=ia(ma);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},
c.valueOf=$a);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;q(ma,function(a,b){var d=N(b);c[fb("parse_as_"+d)]=function(b){return e(a,b)};c[fb("get_trusted_"+d)]=function(b){return f(a,b)};c[fb("trust_as_"+d)]=function(b){return g(a,b)}});return c}]}function xf(){this.$get=["$window","$document",function(a,b){var d={},c=!(a.chrome&&a.chrome.app&&a.chrome.app.runtime)&&a.history&&a.history.pushState,
e=Y((/android (\d+)/.exec(N((a.navigator||{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h,k=/^(Moz|webkit|ms)(?=[A-Z])/,l=g.body&&g.body.style,m=!1,n=!1;if(l){for(var p in l)if(m=k.exec(p)){h=m[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in l&&"webkit");m=!!("transition"in l||h+"Transition"in l);n=!!("animation"in l||h+"Animation"in l);!e||m&&n||(m=y(l.webkitTransition),n=y(l.webkitAnimation))}return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===
a&&11>=Da)return!1;if(z(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ga(),vendorPrefix:h,transitions:m,animations:n,android:e}}]}function zf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$templateCache","$http","$q","$sce",function(b,d,c,e){function f(g,h){f.totalPendingRequests++;y(g)&&b.get(g)||(g=e.getTrustedResourceUrl(g));var k=d.defaults&&d.defaults.transformResponse;M(k)?k=k.filter(function(a){return a!==ac}):k===ac&&(k=null);return d.get(g,
S({cache:b,transformResponse:k},a))["finally"](function(){f.totalPendingRequests--}).then(function(a){b.put(g,a.data);return a.data},function(a){if(!h)throw lg("tpload",g,a.status,a.statusText);return c.reject(a)})}f.totalPendingRequests=0;return f}]}function Af(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];q(a,function(a){var c=ea.element(a).data("$binding");c&&q(c,function(c){d?(new RegExp("(^|\\s)"+
ud(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!=c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var k=a.querySelectorAll("["+g[h]+"model"+(d?"=":"*=")+'"'+b+'"]');if(k.length)return k}},getLocation:function(){return d.url()},setLocation:function(b){b!==d.url()&&(d.url(b),a.$digest())},whenStable:function(a){b.notifyWhenNoOutstandingRequests(a)}}}]}function Bf(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",
function(a,b,d,c,e){function f(f,k,l){D(f)||(l=k,k=f,f=E);var m=Aa.call(arguments,3),n=A(l)&&!l,p=(n?c:d).defer(),q=p.promise,s;s=b.defer(function(){try{p.resolve(f.apply(null,m))}catch(b){p.reject(b),e(b)}finally{delete g[q.$$timeoutId]}n||a.$apply()},k);q.$$timeoutId=s;g[s]=p;return q}var g={};f.cancel=function(a){return a&&a.$$timeoutId in g?(g[a.$$timeoutId].reject("canceled"),delete g[a.$$timeoutId],b.defer.cancel(a.$$timeoutId)):!1};return f}]}function sa(a){Da&&(Z.setAttribute("href",a),a=
Z.href);Z.setAttribute("href",a);return{href:Z.href,protocol:Z.protocol?Z.protocol.replace(/:$/,""):"",host:Z.host,search:Z.search?Z.search.replace(/^\?/,""):"",hash:Z.hash?Z.hash.replace(/^#/,""):"",hostname:Z.hostname,port:Z.port,pathname:"/"===Z.pathname.charAt(0)?Z.pathname:"/"+Z.pathname}}function gd(a){a=y(a)?sa(a):a;return a.protocol===wd.protocol&&a.host===wd.host}function Cf(){this.$get=da(T)}function xd(a){function b(a){try{return decodeURIComponent(a)}catch(b){return a}}var d=a[0]||{},
c={},e="";return function(){var a,g,h,k,l;a=d.cookie||"";if(a!==e)for(e=a,a=e.split("; "),c={},h=0;h<a.length;h++)g=a[h],k=g.indexOf("="),0<k&&(l=b(g.substring(0,k)),z(c[l])&&(c[l]=b(g.substring(k+1))));return c}}function Gf(){this.$get=xd}function Jc(a){function b(d,c){if(J(d)){var e={};q(d,function(a,c){e[c]=b(c,a)});return e}return a.factory(d+"Filter",c)}this.register=b;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];b("currency",yd);b("date",zd);b("filter",mg);
b("json",ng);b("limitTo",og);b("lowercase",pg);b("number",Ad);b("orderBy",Bd);b("uppercase",qg)}function mg(){return function(a,b,d){if(!za(a)){if(null==a)return a;throw O("filter")("notarray",a);}var c;switch(ic(b)){case "function":break;case "boolean":case "null":case "number":case "string":c=!0;case "object":b=rg(b,d,c);break;default:return a}return Array.prototype.filter.call(a,b)}}function rg(a,b,d){var c=J(a)&&"$"in a;!0===b?b=na:D(b)||(b=function(a,b){if(z(a))return!1;if(null===a||null===b)return a===
b;if(J(b)||J(a)&&!rc(a))return!1;a=N(""+a);b=N(""+b);return-1!==a.indexOf(b)});return function(e){return c&&!J(e)?Ma(e,a.$,b,!1):Ma(e,a,b,d)}}function Ma(a,b,d,c,e){var f=ic(a),g=ic(b);if("string"===g&&"!"===b.charAt(0))return!Ma(a,b.substring(1),d,c);if(M(a))return a.some(function(a){return Ma(a,b,d,c)});switch(f){case "object":var h;if(c){for(h in a)if("$"!==h.charAt(0)&&Ma(a[h],b,d,!0))return!0;return e?!1:Ma(a,b,d,!1)}if("object"===g){for(h in b)if(e=b[h],!D(e)&&!z(e)&&(f="$"===h,!Ma(f?a:a[h],
e,d,f,f)))return!1;return!0}return d(a,b);case "function":return!1;default:return d(a,b)}}function ic(a){return null===a?"null":typeof a}function yd(a){var b=a.NUMBER_FORMATS;return function(a,c,e){z(c)&&(c=b.CURRENCY_SYM);z(e)&&(e=b.PATTERNS[1].maxFrac);return null==a?a:Cd(a,b.PATTERNS[1],b.GROUP_SEP,b.DECIMAL_SEP,e).replace(/\u00A4/g,c)}}function Ad(a){var b=a.NUMBER_FORMATS;return function(a,c){return null==a?a:Cd(a,b.PATTERNS[0],b.GROUP_SEP,b.DECIMAL_SEP,c)}}function sg(a){var b=0,d,c,e,f,g;-1<
(c=a.indexOf(Dd))&&(a=a.replace(Dd,""));0<(e=a.search(/e/i))?(0>c&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)==jc;e++);if(e==(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)==jc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Ed&&(d=d.splice(0,Ed-1),b=c-1,c=1);return{d:d,e:b,i:c}}function tg(a,b,d,c){var e=a.d,f=e.length-a.i;b=z(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0<d){e.splice(Math.max(a.i,d));for(var g=d;g<e.length;g++)e[g]=0}else for(f=
Math.max(0,f),a.i=1,e.length=Math.max(1,d=b+1),e[0]=0,g=1;g<d;g++)e[g]=0;if(5<=c)if(0>d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;f<Math.max(0,b);f++)e.push(0);if(b=e.reduceRight(function(a,b,c,d){b+=a;d[c]=b%10;return Math.floor(b/10)},0))e.unshift(b),a.i++}function Cd(a,b,d,c,e){if(!y(a)&&!R(a)||isNaN(a))return"";var f=!isFinite(a),g=!1,h=Math.abs(a)+"",k="";if(f)k="\u221e";else{g=sg(h);tg(g,e,b.minFrac,b.maxFrac);k=g.d;h=g.i;e=g.e;f=[];for(g=k.reduce(function(a,
b){return a&&!b},!0);0>h;)k.unshift(0),h++;0<h?f=k.splice(h):(f=k,k=[0]);h=[];for(k.length>=b.lgSize&&h.unshift(k.splice(-b.lgSize).join(""));k.length>b.gSize;)h.unshift(k.splice(-b.gSize).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+k+b.posSuf}function Jb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length<b;)a=jc+a;d&&(a=a.substr(a.length-b));return e+a}function X(a,b,d,c,e){d=
d||0;return function(f){f=f["get"+a]();if(0<d||f>-d)f+=d;0===f&&-12==d&&(f=12);return Jb(f,b,c,e)}}function lb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Fd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Gd(a){return function(b){var d=Fd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Jb(b,a)}}function kc(a,b){return 0>=a.getFullYear()?
b.ERAS[0]:b.ERAS[1]}function zd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Y(b[9]+b[10]),g=Y(b[9]+b[11]));h.call(a,Y(b[1]),Y(b[2])-1,Y(b[3]));f=Y(b[4]||0)-f;g=Y(b[5]||0)-g;h=Y(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=
[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;y(c)&&(c=ug.test(c)?Y(c):b(c));R(c)&&(c=new Date(c));if(!fa(c)||!isFinite(c.getTime()))return c;for(;d;)(l=vg.exec(d))?(h=cb(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=vc(f,m),c=Rb(c,f,!0));q(h,function(b){k=wg[b];g+=k?k(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ng(){return function(a,b){z(b)&&(b=2);return db(a,b)}}function og(){return function(a,b,d){b=Infinity===
Math.abs(Number(b))?Number(b):Y(b);if(isNaN(b))return a;R(a)&&(a=a.toString());if(!M(a)&&!y(a))return a;d=!d||isNaN(d)?0:Y(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?a.slice(d,d+b):0===d?a.slice(b,a.length):a.slice(Math.max(0,d+b),d)}}function Bd(a){function b(b,d){d=d?-1:1;return b.map(function(b){var c=1,h=$a;if(D(b))h=b;else if(y(b)){if("+"==b.charAt(0)||"-"==b.charAt(0))c="-"==b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(h=a(b),h.constant))var k=h(),h=function(a){return a[k]}}return{get:h,
descending:c*d}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}return function(a,e,f){if(null==a)return a;if(!za(a))throw O("orderBy")("notarray",a);M(e)||(e=[e]);0===e.length&&(e=["+"]);var g=b(e,f);g.push({get:function(){return{}},descending:f?-1:1});a=Array.prototype.map.call(a,function(a,b){return{value:a,predicateValues:g.map(function(c){var e=c.get(a);c=typeof e;if(null===e)c="string",e="null";else if("string"===c)e=e.toLowerCase();else if("object"===
c)a:{if("function"===typeof e.valueOf&&(e=e.valueOf(),d(e)))break a;if(rc(e)&&(e=e.toString(),d(e)))break a;e=b}return{value:e,type:c}})}});a.sort(function(a,b){for(var c=0,d=0,e=g.length;d<e;++d){var c=a.predicateValues[d],f=b.predicateValues[d],q=0;c.type===f.type?c.value!==f.value&&(q=c.value<f.value?-1:1):q=c.type<f.type?-1:1;if(c=q*g[d].descending)break}return c});return a=a.map(function(a){return a.value})}}function Na(a){D(a)&&(a={link:a});a.restrict=a.restrict||"AC";return da(a)}function Hd(a,
b,d,c,e){var f=this,g=[];f.$error={};f.$$success={};f.$pending=u;f.$name=e(b.name||b.ngForm||"")(d);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;f.$$parentForm=Kb;f.$rollbackViewValue=function(){q(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){q(g,function(a){a.$commitViewValue()})};f.$addControl=function(a){Ta(a.$name,"input");g.push(a);a.$name&&(f[a.$name]=a);a.$$parentForm=f};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=
a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];q(f.$pending,function(b,c){f.$setValidity(c,null,a)});q(f.$error,function(b,c){f.$setValidity(c,null,a)});q(f.$$success,function(b,c){f.$setValidity(c,null,a)});bb(g,a);a.$$parentForm=Kb};Id({ctrl:this,$element:a,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(bb(d,c),0===d.length&&delete a[b])},$animate:c});f.$setDirty=function(){c.removeClass(a,Xa);c.addClass(a,
Lb);f.$dirty=!0;f.$pristine=!1;f.$$parentForm.$setDirty()};f.$setPristine=function(){c.setClass(a,Xa,Lb+" ng-submitted");f.$dirty=!1;f.$pristine=!0;f.$submitted=!1;q(g,function(a){a.$setPristine()})};f.$setUntouched=function(){q(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){c.addClass(a,"ng-submitted");f.$submitted=!0;f.$$parentForm.$setSubmitted()}}function lc(a){a.$formatters.push(function(b){return a.$isEmpty(b)?b:b.toString()})}function mb(a,b,d,c,e,f){var g=N(b[0].type);if(!e.android){var h=
!1;b.on("compositionstart",function(){h=!0});b.on("compositionend",function(){h=!1;l()})}var k,l=function(a){k&&(f.defer.cancel(k),k=null);if(!h){var e=b.val();a=a&&a.type;"password"===g||d.ngTrim&&"false"===d.ngTrim||(e=W(e));(c.$viewValue!==e||""===e&&c.$$hasNativeValidators)&&c.$setViewValue(e,a)}};if(e.hasEvent("input"))b.on("input",l);else{var m=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};b.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||
m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut",m)}b.on("change",l);if(Jd[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Mb(a,b){return function(d,c){var e,f;if(fa(d))return d;if(y(d)){'"'==d.charAt(0)&&'"'==d.charAt(d.length-
1)&&(d=d.substring(1,d.length-1));if(xg.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(a,c){c<b.length&&(f[b[c]]=+a)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function nb(a,b,d,c){return function(e,f,g,h,k,l,m){function n(a){return a&&!(a.getTime&&
a.getTime()!==a.getTime())}function p(a){return A(a)&&!fa(a)?d(a)||u:a}Kd(e,f,g,h);mb(e,f,g,h,k,l);var q=h&&h.$options&&h.$options.timezone,s;h.$$parserName=a;h.$parsers.push(function(a){return h.$isEmpty(a)?null:b.test(a)?(a=d(a,s),q&&(a=Rb(a,q)),a):u});h.$formatters.push(function(a){if(a&&!fa(a))throw ob("datefmt",a);if(n(a))return(s=a)&&q&&(s=Rb(s,q,!0)),m("date")(a,c,q);s=null;return""});if(A(g.min)||g.ngMin){var x;h.$validators.min=function(a){return!n(a)||z(x)||d(a)>=x};g.$observe("min",function(a){x=
p(a);h.$validate()})}if(A(g.max)||g.ngMax){var r;h.$validators.max=function(a){return!n(a)||z(r)||d(a)<=r};g.$observe("max",function(a){r=p(a);h.$validate()})}}}function Kd(a,b,d,c){(c.$$hasNativeValidators=J(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?u:a})}function Ld(a,b,d,c,e){if(A(c)){a=a(c);if(!a.constant)throw ob("constexpr",d,c);return a(b)}return e}function mc(a,b){a="ngClass"+a;return["$animate",function(d){function c(a,b){var c=
[],d=0;a:for(;d<a.length;d++){for(var e=a[d],m=0;m<b.length;m++)if(e==b[m])continue a;c.push(e)}return c}function e(a){var b=[];return M(a)?(q(a,function(a){b=b.concat(e(a))}),b):y(a)?a.split(" "):J(a)?(q(a,function(a,c){a&&(b=b.concat(c.split(" ")))}),b):a}return{restrict:"AC",link:function(f,g,h){function k(a,b){var c=g.data("$classCounts")||V(),d=[];q(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}function l(a){if(!0===b||f.$index%
2===b){var l=e(a||[]);if(!m){var q=k(l,1);h.$addClass(q)}else if(!na(a,m)){var s=e(m),q=c(l,s),l=c(s,l),q=k(q,1),l=k(l,-1);q&&q.length&&d.addClass(g,q);l&&l.length&&d.removeClass(g,l)}}m=ia(a)}var m;f.$watch(h[a],l,!0);h.$observe("class",function(b){l(f.$eval(h[a]))});"ngClass"!==a&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var l=e(f.$eval(h[a]));g===b?(g=k(l,1),h.$addClass(g)):(g=k(l,-1),h.$removeClass(g))}})}}}]}function Id(a){function b(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&
f[a]&&(k.removeClass(e,a),f[a]=!1)}function d(a,c){a=a?"-"+zc(a,"-"):"";b(pb+a,!0===c);b(Md+a,!1===c)}var c=a.ctrl,e=a.$element,f={},g=a.set,h=a.unset,k=a.$animate;f[Md]=!(f[pb]=e.hasClass(pb));c.$setValidity=function(a,e,f){z(e)?(c.$pending||(c.$pending={}),g(c.$pending,a,f)):(c.$pending&&h(c.$pending,a,f),Nd(c.$pending)&&(c.$pending=u));Oa(e)?e?(h(c.$error,a,f),g(c.$$success,a,f)):(g(c.$error,a,f),h(c.$$success,a,f)):(h(c.$error,a,f),h(c.$$success,a,f));c.$pending?(b(Od,!0),c.$valid=c.$invalid=
u,d("",null)):(b(Od,!1),c.$valid=Nd(c.$error),c.$invalid=!c.$valid,d("",c.$valid));e=c.$pending&&c.$pending[a]?u:c.$error[a]?!1:c.$$success[a]?!0:null;d(a,e);c.$$parentForm.$setValidity(a,e,c)}}function Nd(a){if(a)for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}var yg=/^\/(.+)\/([a-z]*)$/,va=Object.prototype.hasOwnProperty,N=function(a){return y(a)?a.toLowerCase():a},vb=function(a){return y(a)?a.toUpperCase():a},Da,H,$,Aa=[].slice,Yf=[].splice,zg=[].push,ka=Object.prototype.toString,sc=Object.getPrototypeOf,
Ba=O("ng"),ea=T.angular||(T.angular={}),Tb,qb=0;Da=P.documentMode;E.$inject=[];$a.$inject=[];var M=Array.isArray,Zd=/^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/,W=function(a){return y(a)?a.trim():a},ud=function(a){return a.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},Ga=function(){if(!A(Ga.rules)){var a=P.querySelector("[ng-csp]")||P.querySelector("[data-ng-csp]");if(a){var b=a.getAttribute("ng-csp")||a.getAttribute("data-ng-csp");
Ga.rules={noUnsafeEval:!b||-1!==b.indexOf("no-unsafe-eval"),noInlineStyle:!b||-1!==b.indexOf("no-inline-style")}}else{a=Ga;try{new Function(""),b=!1}catch(d){b=!0}a.rules={noUnsafeEval:b,noInlineStyle:!1}}}return Ga.rules},sb=function(){if(A(sb.name_))return sb.name_;var a,b,d=Qa.length,c,e;for(b=0;b<d;++b)if(c=Qa[b],a=P.querySelector("["+c.replace(":","\\:")+"jq]")){e=a.getAttribute(c+"jq");break}return sb.name_=e},be=/:/g,Qa=["ng-","data-ng-","ng:","x-ng-"],ge=/[A-Z]/g,Ac=!1,Pa=3,ke={full:"1.5.3",
major:1,minor:5,dot:3,codeName:"diplohaplontic-meiosis"};U.expando="ng339";var hb=U.cache={},Mf=1;U._data=function(a){return this.cache[a[this.expando]]||{}};var Hf=/([\:\-\_]+(.))/g,If=/^moz([A-Z])/,zb={mouseleave:"mouseout",mouseenter:"mouseover"},Vb=O("jqLite"),Lf=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,Ub=/<|&#?\w+;/,Jf=/<([\w:-]+)/,Kf=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ha={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,
"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var Rf=Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Ra=U.prototype={ready:function(a){function b(){d||(d=!0,a())}var d=!1;"complete"===P.readyState?setTimeout(b):(this.on("DOMContentLoaded",b),U(T).on("load",b))},toString:function(){var a=
[];q(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?H(this[a]):H(this[this.length+a])},length:0,push:zg,sort:[].sort,splice:[].splice},Eb={};q("multiple selected checked disabled readOnly required open".split(" "),function(a){Eb[N(a)]=a});var Sc={};q("input select option textarea button form details".split(" "),function(a){Sc[a]=!0});var $c={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};q({data:Xb,removeData:gb,
hasData:function(a){for(var b in hb[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b<d;b++)gb(a[b])}},function(a,b){U[b]=a});q({data:Xb,inheritedData:Db,scope:function(a){return H.data(a,"$scope")||Db(a.parentNode||a,["$isolateScope","$scope"])},isolateScope:function(a){return H.data(a,"$isolateScope")||H.data(a,"$isolateScopeNoTemplate")},controller:Pc,injector:function(a){return Db(a,"$injector")},removeAttr:function(a,b){a.removeAttribute(b)},hasClass:Ab,css:function(a,
b,d){b=fb(b);if(A(d))a.style[b]=d;else return a.style[b]},attr:function(a,b,d){var c=a.nodeType;if(c!==Pa&&2!==c&&8!==c)if(c=N(b),Eb[c])if(A(d))d?(a[b]=!0,a.setAttribute(b,c)):(a[b]=!1,a.removeAttribute(c));else return a[b]||(a.attributes.getNamedItem(b)||E).specified?c:u;else if(A(d))a.setAttribute(b,d);else if(a.getAttribute)return a=a.getAttribute(b,2),null===a?u:a},prop:function(a,b,d){if(A(d))a[b]=d;else return a[b]},text:function(){function a(a,d){if(z(d)){var c=a.nodeType;return 1===c||c===
Pa?a.textContent:""}a.textContent=d}a.$dv="";return a}(),val:function(a,b){if(z(b)){if(a.multiple&&"select"===oa(a)){var d=[];q(a.options,function(a){a.selected&&d.push(a.value||a.text)});return 0===d.length?null:d}return a.value}a.value=b},html:function(a,b){if(z(b))return a.innerHTML;xb(a,!0);a.innerHTML=b},empty:Qc},function(a,b){U.prototype[b]=function(b,c){var e,f,g=this.length;if(a!==Qc&&z(2==a.length&&a!==Ab&&a!==Pc?b:c)){if(J(b)){for(e=0;e<g;e++)if(a===Xb)a(this[e],b);else for(f in b)a(this[e],
f,b[f]);return this}e=a.$dv;g=z(e)?Math.min(g,1):g;for(f=0;f<g;f++){var h=a(this[f],b,c);e=e?e+h:h}return e}for(e=0;e<g;e++)a(this[e],b,c);return this}});q({removeData:gb,on:function(a,b,d,c){if(A(c))throw Vb("onargs");if(Kc(a)){c=yb(a,!0);var e=c.events,f=c.handle;f||(f=c.handle=Of(a,e));c=0<=b.indexOf(" ")?b.split(" "):[b];for(var g=c.length,h=function(b,c,g){var h=e[b];h||(h=e[b]=[],h.specialHandlerWrapper=c,"$destroy"===b||g||a.addEventListener(b,f,!1));h.push(d)};g--;)b=c[g],zb[b]?(h(zb[b],Qf),
h(b,u,!0)):h(b)}},off:Oc,one:function(a,b,d){a=H(a);a.on(b,function e(){a.off(b,d);a.off(b,e)});a.on(b,d)},replaceWith:function(a,b){var d,c=a.parentNode;xb(a);q(new U(b),function(b){d?c.insertBefore(b,d.nextSibling):c.replaceChild(b,a);d=b})},children:function(a){var b=[];q(a.childNodes,function(a){1===a.nodeType&&b.push(a)});return b},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,b){var d=a.nodeType;if(1===d||11===d){b=new U(b);for(var d=0,c=b.length;d<c;d++)a.appendChild(b[d])}},
prepend:function(a,b){if(1===a.nodeType){var d=a.firstChild;q(new U(b),function(b){a.insertBefore(b,d)})}},wrap:function(a,b){Mc(a,H(b).eq(0).clone()[0])},remove:Yb,detach:function(a){Yb(a,!0)},after:function(a,b){var d=a,c=a.parentNode;b=new U(b);for(var e=0,f=b.length;e<f;e++){var g=b[e];c.insertBefore(g,d.nextSibling);d=g}},addClass:Cb,removeClass:Bb,toggleClass:function(a,b,d){b&&q(b.split(" "),function(b){var e=d;z(e)&&(e=!Ab(a,b));(e?Cb:Bb)(a,b)})},parent:function(a){return(a=a.parentNode)&&
11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,b){return a.getElementsByTagName?a.getElementsByTagName(b):[]},clone:Wb,triggerHandler:function(a,b,d){var c,e,f=b.type||b,g=yb(a);if(g=(g=g&&g.events)&&g[f])c={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},
stopPropagation:E,type:f,target:a},b.type&&(c=S(c,b)),b=ia(g),e=d?[c].concat(d):[c],q(b,function(b){c.isImmediatePropagationStopped()||b.apply(a,e)})}},function(a,b){U.prototype[b]=function(b,c,e){for(var f,g=0,h=this.length;g<h;g++)z(f)?(f=a(this[g],b,c,e),A(f)&&(f=H(f))):Nc(f,a(this[g],b,c,e));return A(f)?f:this};U.prototype.bind=U.prototype.on;U.prototype.unbind=U.prototype.off});Ua.prototype={put:function(a,b){this[Ha(a,this.nextUid)]=b},get:function(a){return this[Ha(a,this.nextUid)]},remove:function(a){var b=
this[a=Ha(a,this.nextUid)];delete this[a];return b}};var Ff=[function(){this.$get=[function(){return Ua}]}],Tf=/^([^\(]+?)=>/,Uf=/^[^\(]*\(\s*([^\)]*)\)/m,Ag=/,/,Bg=/^\s*(_?)(\S+?)\1\s*$/,Sf=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ia=O("$injector");eb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw y(d)&&d||(d=a.name||Vf(a)),Ia("strictdi",d);b=Tc(a);q(b[1].split(Ag),function(a){a.replace(Bg,function(a,b,d){c.push(d)})})}a.$inject=c}}else M(a)?
(b=a.length-1,Sa(a[b],"fn"),c=a.slice(0,b)):Sa(a,"fn",!0);return c};var Pd=O("$animate"),Ye=function(){this.$get=E},Ze=function(){var a=new Ua,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=y(b)?b.split(" "):M(b)?b:[],q(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){q(b,function(b){var c=a.get(b);if(c){var d=Wf(b.attr("class")),e="",f="";q(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});q(b,function(a){e&&Cb(a,
e);f&&Bb(a,f)});a.remove(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.put(g,k),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},We=["$provide",function(a){var b=this;this.$$registeredAnimations=Object.create(null);this.register=function(d,c){if(d&&"."!==d.charAt(0))throw Pd("notcsel",
d);var e=d+"-animation";b.$$registeredAnimations[d.substr(1)]=e;a.factory(e,c)};this.classNameFilter=function(a){if(1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null)&&/(\s+|\/)ng-animate(\s+|\/)/.test(this.$$classNameFilter.toString()))throw Pd("nongcls","ng-animate");return this.$$classNameFilter};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var h;a:{for(h=0;h<d.length;h++){var k=d[h];if(1===k.nodeType){h=k;break a}}h=void 0}!h||h.parentNode||h.previousElementSibling||
(d=null)}d?d.after(a):c.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&a.end()},enter:function(e,f,g,h){f=f&&H(f);g=g&&H(g);f=f||g.parent();b(e,f,g);return a.push(e,"enter",Ja(h))},move:function(e,f,g,h){f=f&&H(f);g=g&&H(g);f=f||g.parent();b(e,f,g);return a.push(e,"move",Ja(h))},leave:function(b,c){return a.push(b,"leave",Ja(c),function(){b.remove()})},addClass:function(b,c,g){g=Ja(g);g.addClass=ib(g.addclass,c);return a.push(b,"addClass",g)},removeClass:function(b,
c,g){g=Ja(g);g.removeClass=ib(g.removeClass,c);return a.push(b,"removeClass",g)},setClass:function(b,c,g,h){h=Ja(h);h.addClass=ib(h.addClass,c);h.removeClass=ib(h.removeClass,g);return a.push(b,"setClass",h)},animate:function(b,c,g,h,k){k=Ja(k);k.from=k.from?S(k.from,c):c;k.to=k.to?S(k.to,g):g;k.tempClasses=ib(k.tempClasses,h||"ng-inline-animate");return a.push(b,"animate",k)}}}]}],af=function(){this.$get=["$$rAF",function(a){function b(b){d.push(b);1<d.length||a(function(){for(var a=0;a<d.length;a++)d[a]();
d=[]})}var d=[];return function(){var a=!1;b(function(){a=!0});return function(d){a?d():b(d)}}}]},$e=function(){this.$get=["$q","$sniffer","$$animateAsyncRun","$document","$timeout",function(a,b,d,c,e){function f(a){this.setHost(a);var b=d();this._doneCallbacks=[];this._tick=function(a){var d=c[0];d&&d.hidden?e(a,0,!1):b(a)};this._state=0}f.chain=function(a,b){function c(){if(d===a.length)b(!0);else a[d](function(a){!1===a?b(!1):(d++,c())})}var d=0;c()};f.all=function(a,b){function c(f){e=e&&f;++d===
a.length&&b(e)}var d=0,e=!0;q(a,function(a){a.done(c)})};f.prototype={setHost:function(a){this.host=a||{}},done:function(a){2===this._state?a():this._doneCallbacks.push(a)},progress:E,getPromise:function(){if(!this.promise){var b=this;this.promise=a(function(a,c){b.done(function(b){!1===b?c():a()})})}return this.promise},then:function(a,b){return this.getPromise().then(a,b)},"catch":function(a){return this.getPromise()["catch"](a)},"finally":function(a){return this.getPromise()["finally"](a)},pause:function(){this.host.pause&&
this.host.pause()},resume:function(){this.host.resume&&this.host.resume()},end:function(){this.host.end&&this.host.end();this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel();this._resolve(!1)},complete:function(a){var b=this;0===b._state&&(b._state=1,b._tick(function(){b._resolve(a)}))},_resolve:function(a){2!==this._state&&(q(this._doneCallbacks,function(b){b(a)}),this._doneCallbacks.length=0,this._state=2)}};return f}]},Xe=function(){this.$get=["$$rAF","$q","$$AnimateRunner",
function(a,b,d){return function(b,e){function f(){a(function(){g.addClass&&(b.addClass(g.addClass),g.addClass=null);g.removeClass&&(b.removeClass(g.removeClass),g.removeClass=null);g.to&&(b.css(g.to),g.to=null);h||k.complete();h=!0});return k}var g=e||{};g.$$prepared||(g=pa(g));g.cleanupStyles&&(g.from=g.to=null);g.from&&(b.css(g.from),g.from=null);var h,k=new d;return{start:f,end:f}}}]},ga=O("$compile");Cc.$inject=["$provide","$$sanitizeUriProvider"];var Vc=/^((?:x|data)[\:\-_])/i,Zf=O("$controller"),
ad=/^(\S+)(\s+as\s+([\w$]+))?$/,gf=function(){this.$get=["$document",function(a){return function(b){b?!b.nodeType&&b instanceof H&&(b=b[0]):b=a[0].body;return b.offsetWidth+1}}]},bd="application/json",bc={"Content-Type":bd+";charset=utf-8"},ag=/^\[|^\{(?!\{)/,bg={"[":/]$/,"{":/}$/},$f=/^\)\]\}',?\n/,Cg=O("$http"),fd=function(a){return function(){throw Cg("legacy",a);}},La=ea.$interpolateMinErr=O("$interpolate");La.throwNoconcat=function(a){throw La("noconcat",a);};La.interr=function(a,b){return La("interr",
a,b.toString())};var Dg=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,dg={http:80,https:443,ftp:21},Fb=O("$location"),Eg={$$html5:!1,$$replace:!1,absUrl:Gb("$$absUrl"),url:function(a){if(z(a))return this.$$url;var b=Dg.exec(a);(b[1]||""===a)&&this.path(decodeURIComponent(b[1]));(b[2]||b[1]||""===a)&&this.search(b[3]||"");this.hash(b[5]||"");return this},protocol:Gb("$$protocol"),host:Gb("$$host"),port:Gb("$$port"),path:kd("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,
b){switch(arguments.length){case 0:return this.$$search;case 1:if(y(a)||R(a))a=a.toString(),this.$$search=xc(a);else if(J(a))a=pa(a,{}),q(a,function(b,c){null==b&&delete a[c]}),this.$$search=a;else throw Fb("isrcharg");break;default:z(b)||null===b?delete this.$$search[a]:this.$$search[a]=b}this.$$compose();return this},hash:kd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};q([jd,ec,dc],function(a){a.prototype=Object.create(Eg);a.prototype.state=
function(b){if(!arguments.length)return this.$$state;if(a!==dc||!this.$$html5)throw Fb("nostate");this.$$state=z(b)?null:b;return this}});var ca=O("$parse"),fg=Function.prototype.call,gg=Function.prototype.apply,hg=Function.prototype.bind,Nb=V();q("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Nb[a]=!0});var Fg={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},gc=function(a){this.options=a};gc.prototype={constructor:gc,lex:function(a){this.text=a;this.index=0;for(this.tokens=
[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var b=a+this.peek(),d=b+this.peek(2),c=Nb[b],e=Nb[d];Nb[a]||c||e?(a=e?d:c?b:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=
a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,b){return-1!==b.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===
a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=A(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw ca("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index<this.text.length;){var d=N(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var c=this.peek();if("e"==d&&this.isExpOperator(c))a+=d;else if(this.isExpOperator(d)&&c&&this.isNumber(c)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||
c&&this.isNumber(c)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:b,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=this.index;this.index<this.text.length;){var b=this.text.charAt(this.index);if(!this.isIdent(b)&&!this.isNumber(b))break;this.index++}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var b=this.index;this.index++;for(var d="",c=a,e=!1;this.index<this.text.length;){var f=
this.text.charAt(this.index),c=c+f;if(e)"u"===f?(e=this.text.substring(this.index+1,this.index+5),e.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+e+"]"),this.index+=4,d+=String.fromCharCode(parseInt(e,16))):d+=Fg[f]||f,e=!1;else if("\\"===f)e=!0;else{if(f===a){this.index++;this.tokens.push({index:b,text:c,constant:!0,value:d});return}d+=f}this.index++}this.throwError("Unterminated quote",b)}};var s=function(a,b){this.lexer=a;this.options=b};s.Program="Program";s.ExpressionStatement=
"ExpressionStatement";s.AssignmentExpression="AssignmentExpression";s.ConditionalExpression="ConditionalExpression";s.LogicalExpression="LogicalExpression";s.BinaryExpression="BinaryExpression";s.UnaryExpression="UnaryExpression";s.CallExpression="CallExpression";s.MemberExpression="MemberExpression";s.Identifier="Identifier";s.Literal="Literal";s.ArrayExpression="ArrayExpression";s.Property="Property";s.ObjectExpression="ObjectExpression";s.ThisExpression="ThisExpression";s.LocalsExpression="LocalsExpression";
s.NGValueParameter="NGValueParameter";s.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:s.Program,body:a}},expressionStatement:function(){return{type:s.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=
this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();this.expect("=")&&(a={type:s.AssignmentExpression,left:a,right:this.assignment(),operator:"="});return a},ternary:function(){var a=this.logicalOR(),b,d;return this.expect("?")&&(b=this.expression(),this.consume(":"))?(d=this.expression(),{type:s.ConditionalExpression,test:a,alternate:b,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a=
{type:s.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:s.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),b;b=this.expect("==","!=","===","!==");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),b;b=this.expect("<",">","<=",">=");)a={type:s.BinaryExpression,
operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:
this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=pa(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",
this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());
return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.expression());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");
return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?b.key=this.constant():this.peek().identifier?b.key=this.identifier():this.throwError("invalid key",this.peek());this.consume(":");b.value=this.expression();a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw ca("syntax",b.text,a,b.index+1,this.text,
this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw ca("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw ca("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},
expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression},$locals:{type:s.LocalsExpression}}};rd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:b,fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};aa(c,d.$filter);var e="",f;this.stage="assign";if(f=pd(c))this.state.computing="assign",e=this.nextId(),this.recurse(f,e),this.return_(e),e="fn.assign="+
this.generateFunction("assign","s,v,l");f=nd(c.body);d.stage="inputs";q(f,function(a,b){var c="fn"+b;d.state[c]={vars:[],body:[],own:{}};d.state.computing=c;var e=d.nextId();d.recurse(a,e);d.return_(e);d.state.inputs.push(c);a.watchId=b});this.state.computing="fn";this.stage="main";this.recurse(c);e='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+e+this.watchFns()+"return fn;";e=(new Function("$filter","ensureSafeMemberName","ensureSafeObject",
"ensureSafeFunction","getStringValue","ensureSafeAssignContext","ifDefined","plus","text",e))(this.$filter,Wa,ta,ld,eg,Hb,ig,md,a);this.state=this.stage=u;e.literal=qd(c);e.constant=c.constant;return e},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;q(b,function(b){a.push("var "+b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+
"};"},filterPrefix:function(){var a=[],b=this;q(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m;c=c||E;if(!f&&A(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,
c,e,!0));else switch(a.type){case s.Program:q(a.body,function(b,c){k.recurse(b.expression,u,u,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(m);break;case s.UnaryExpression:this.recurse(a.argument,u,u,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,u,u,function(a){g=a});this.recurse(a.right,u,u,function(a){h=a});m="+"===
a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right,b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?
"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);Wa(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));(k.state.expensiveChecks||Ib(a.name))&&k.addEnsureSafeObject(b);c(b);
break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,u,function(){k.if_(k.notNull(g),function(){e&&1!==e&&k.addEnsureSafeAssignContext(g);if(a.computed)h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),k.addEnsureSafeMemberName(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.ensureSafeObject(k.computedMember(g,h)),k.assign(b,m),d&&(d.computed=!0,d.name=h);else{Wa(a.property.name);e&&
1!==e&&k.if_(k.not(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}"));m=k.nonComputedMember(g,a.property.name);if(k.state.expensiveChecks||Ib(a.property.name))m=k.ensureSafeObject(m);k.assign(b,m);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],q(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+
l.join(",")+")",k.assign(b,m),c(b)):(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){k.addEnsureSafeFunction(h);q(a.arguments,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(k.ensureSafeObject(a))})});g.name?(k.state.expensiveChecks||k.addEnsureSafeObject(g.context),m=k.member(g.context,g.name,g.computed)+"("+l.join(",")+")"):m=h+"("+l.join(",")+")";m=k.ensureSafeObject(m);k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=
this.nextId();g={};if(!od(a.left))throw ca("lval");this.recurse(a.left,u,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);k.addEnsureSafeObject(k.member(g.context,g.name,g.computed));k.addEnsureSafeAssignContext(g.context);m=k.member(g.context,g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];q(a.elements,function(a){k.recurse(a,k.nextId(),u,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(m);break;case s.ObjectExpression:l=
[];q(a.properties,function(a){k.recurse(a.value,k.nextId(),u,function(b){l.push(k.escape(a.key.type===s.Identifier?a.key.name:""+a.key.value)+":"+b)})});m="{"+l.join(",")+"}";this.assign(b,m);c(m);break;case s.ThisExpression:this.assign(b,"s");c("s");break;case s.LocalsExpression:this.assign(b,"l");c("l");break;case s.NGValueParameter:this.assign(b,"v"),c("v")}},getHasOwnProperty:function(a,b){var d=a+"."+b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+
a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");
d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){return a+"."+b},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a,b)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),";")},addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),
";")},addEnsureSafeAssignContext:function(a){this.current().body.push(this.ensureSafeAssignContext(a),";")},ensureSafeObject:function(a){return"ensureSafeObject("+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},ensureSafeAssignContext:function(a){return"ensureSafeAssignContext("+a+",text)"},lazyRecurse:function(a,b,d,c,e,f){var g=
this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(y(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(R(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw ca("esc");},nextId:function(a,
b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};sd.prototype={compile:function(a,b){var d=this,c=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=b;aa(c,d.$filter);var e,f;if(e=pd(c))f=this.recurse(e);e=nd(c.body);var g;e&&(g=[],q(e,function(a,b){var c=d.recurse(a);a.input=c;g.push(c);a.watchId=b}));var h=[];q(c.body,function(a){h.push(d.recurse(a.expression))});e=0===c.body.length?E:1===
c.body.length?h[0]:function(a,b){var c;q(h,function(d){c=d(a,b)});return c};f&&(e.assign=function(a,b,c){return f(a,c,b)});g&&(e.inputs=g);e.literal=qd(c);e.constant=c.constant;return e},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),
this["binary"+a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return Wa(a.name,f.expression),f.identifier(a.name,f.expensiveChecks||Ib(a.name),b,d,f.expression);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(Wa(a.property.name,f.expression),
e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d,f.expression):this.nonComputedMember(c,e,f.expensiveChecks,b,d,f.expression);case s.CallExpression:return g=[],q(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var n=[],p=0;p<g.length;++p)n.push(g[p](a,c,d,f));a=e.apply(u,n,f);return b?{context:u,name:u,value:a}:a}:function(a,c,d,m){var n=
e(a,c,d,m),p;if(null!=n.value){ta(n.context,f.expression);ld(n.value,f.expression);p=[];for(var q=0;q<g.length;++q)p.push(ta(g[q](a,c,d,m),f.expression));p=ta(n.value.apply(n.context,p),f.expression)}return b?{value:p}:p};case s.AssignmentExpression:return c=this.recurse(a.left,!0,1),e=this.recurse(a.right),function(a,d,g,m){var n=c(a,d,g,m);a=e(a,d,g,m);ta(n.value,f.expression);Hb(n.context);n.context[n.name]=a;return b?{value:a}:a};case s.ArrayExpression:return g=[],q(a.elements,function(a){g.push(f.recurse(a))}),
function(a,c,d,e){for(var f=[],p=0;p<g.length;++p)f.push(g[p](a,c,d,e));return b?{value:f}:f};case s.ObjectExpression:return g=[],q(a.properties,function(a){g.push({key:a.key.type===s.Identifier?a.key.name:""+a.key.value,value:f.recurse(a.value)})}),function(a,c,d,e){for(var f={},p=0;p<g.length;++p)f[g[p].key]=g[p].value(a,c,d,e);return b?{value:f}:f};case s.ThisExpression:return function(a){return b?{value:a}:a};case s.LocalsExpression:return function(a,c){return b?{value:c}:c};case s.NGValueParameter:return function(a,
c,d){return b?{value:d}:d}}},"unary+":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=A(d)?+d:0;return b?{value:d}:d}},"unary-":function(a,b){return function(d,c,e,f){d=a(d,c,e,f);d=A(d)?-d:0;return b?{value:d}:d}},"unary!":function(a,b){return function(d,c,e,f){d=!a(d,c,e,f);return b?{value:d}:d}},"binary+":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);h=md(h,c);return d?{value:h}:h}},"binary-":function(a,b,d){return function(c,e,f,g){var h=a(c,e,f,g);c=b(c,e,f,g);
h=(A(h)?h:0)-(A(c)?c:0);return d?{value:h}:h}},"binary*":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)*b(c,e,f,g);return d?{value:c}:c}},"binary/":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)/b(c,e,f,g);return d?{value:c}:c}},"binary%":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)%b(c,e,f,g);return d?{value:c}:c}},"binary===":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)===b(c,e,f,g);return d?{value:c}:c}},"binary!==":function(a,b,d){return function(c,e,f,g){c=a(c,
e,f,g)!==b(c,e,f,g);return d?{value:c}:c}},"binary==":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)==b(c,e,f,g);return d?{value:c}:c}},"binary!=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)!=b(c,e,f,g);return d?{value:c}:c}},"binary<":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<b(c,e,f,g);return d?{value:c}:c}},"binary>":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,
g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:u,
name:u,value:a}:a}},identifier:function(a,b,d,c,e){return function(f,g,h,k){f=g&&a in g?g:f;c&&1!==c&&f&&!f[a]&&(f[a]={});g=f?f[a]:u;b&&ta(g,e);return d?{context:f,name:a,value:g}:g}},computedMember:function(a,b,d,c,e){return function(f,g,h,k){var l=a(f,g,h,k),m,n;null!=l&&(m=b(f,g,h,k),m+="",Wa(m,e),c&&1!==c&&(Hb(l),l&&!l[m]&&(l[m]={})),n=l[m],ta(n,e));return d?{context:l,name:m,value:n}:n}},nonComputedMember:function(a,b,d,c,e,f){return function(g,h,k,l){g=a(g,h,k,l);e&&1!==e&&(Hb(g),g&&!g[b]&&
(g[b]={}));h=null!=g?g[b]:u;(d||Ib(b))&&ta(h,f);return c?{context:g,name:b,value:h}:h}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};var hc=function(a,b,d){this.lexer=a;this.$filter=b;this.options=d;this.ast=new s(a,d);this.astCompiler=d.csp?new sd(this.ast,b):new rd(this.ast,b)};hc.prototype={constructor:hc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};var jg=Object.prototype.valueOf,ua=O("$sce"),ma={HTML:"html",CSS:"css",URL:"url",
RESOURCE_URL:"resourceUrl",JS:"js"},lg=O("$compile"),Z=P.createElement("a"),wd=sa(T.location.href);xd.$inject=["$document"];Jc.$inject=["$provide"];var Ed=22,Dd=".",jc="0";yd.$inject=["$locale"];Ad.$inject=["$locale"];var wg={yyyy:X("FullYear",4,0,!1,!0),yy:X("FullYear",2,0,!0,!0),y:X("FullYear",1,0,!1,!0),MMMM:lb("Month"),MMM:lb("Month",!0),MM:X("Month",2,1),M:X("Month",1,1),LLLL:lb("Month",!1,!0),dd:X("Date",2),d:X("Date",1),HH:X("Hours",2),H:X("Hours",1),hh:X("Hours",2,-12),h:X("Hours",1,-12),
mm:X("Minutes",2),m:X("Minutes",1),ss:X("Seconds",2),s:X("Seconds",1),sss:X("Milliseconds",3),EEEE:lb("Day"),EEE:lb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Jb(Math[0<a?"floor":"ceil"](a/60),2)+Jb(Math.abs(a%60),2))},ww:Gd(2),w:Gd(1),G:kc,GG:kc,GGG:kc,GGGG:function(a,b){return 0>=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},vg=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
ug=/^\-?\d+$/;zd.$inject=["$locale"];var pg=da(N),qg=da(vb);Bd.$inject=["$parse"];var me=da({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ka.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};q(Eb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!=a){var c=ya("ng-"+b),e=d;"checked"===a&&(e=function(a,
b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});q($c,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"==e.ngPattern.charAt(0)&&(c=e.ngPattern.match(yg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});q(["src","srcset","href"],function(a){var b=ya("ng-"+a);wb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===
ka.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b),Da&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Kb={$addControl:E,$$renameControl:function(a,b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E};Hd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Qd=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",
restrict:a?"EAC":"E",require:["form","^^?form"],controller:Hd,compile:function(d,f){d.addClass(Xa).addClass(pb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in e)){var p=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",p,!1);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",p,!1)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var q=g?c(n.$name):E;g&&
(q(a,n),e.$observe(g,function(b){n.$name!==b&&(q(a,u),n.$$parentForm.$$renameControl(n,b),q=c(n.$name),q(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);q(a,u);S(n,Kb)})}}}}}]},ne=Qd(),Ae=Qd(!0),xg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,Gg=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,Hg=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,
Ig=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Rd=/^(\d{4,})-(\d{2})-(\d{2})$/,Sd=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,nc=/^(\d{4,})-W(\d\d)$/,Td=/^(\d{4,})-(\d\d)$/,Ud=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Jd=V();q(["date","datetime-local","month","time","week"],function(a){Jd[a]=!0});var Vd={text:function(a,b,d,c,e,f){mb(a,b,d,c,e,f);lc(c)},date:nb("date",Rd,Mb(Rd,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":nb("datetimelocal",Sd,Mb(Sd,"yyyy MM dd HH mm ss sss".split(" ")),
"yyyy-MM-ddTHH:mm:ss.sss"),time:nb("time",Ud,Mb(Ud,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:nb("week",nc,function(a,b){if(fa(a))return a;if(y(a)){nc.lastIndex=0;var d=nc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Fd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:nb("month",Td,Mb(Td,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Kd(a,b,d,c);mb(a,b,d,c,e,f);c.$$parserName=
"number";c.$parsers.push(function(a){return c.$isEmpty(a)?null:Ig.test(a)?parseFloat(a):u});c.$formatters.push(function(a){if(!c.$isEmpty(a)){if(!R(a))throw ob("numfmt",a);a=a.toString()}return a});if(A(d.min)||d.ngMin){var g;c.$validators.min=function(a){return c.$isEmpty(a)||z(g)||a>=g};d.$observe("min",function(a){A(a)&&!R(a)&&(a=parseFloat(a,10));g=R(a)&&!isNaN(a)?a:u;c.$validate()})}if(A(d.max)||d.ngMax){var h;c.$validators.max=function(a){return c.$isEmpty(a)||z(h)||a<=h};d.$observe("max",function(a){A(a)&&
!R(a)&&(a=parseFloat(a,10));h=R(a)&&!isNaN(a)?a:u;c.$validate()})}},url:function(a,b,d,c,e,f){mb(a,b,d,c,e,f);lc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||Gg.test(d)}},email:function(a,b,d,c,e,f){mb(a,b,d,c,e,f);lc(c);c.$$parserName="email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||Hg.test(d)}},radio:function(a,b,d,c){z(d.name)&&b.attr("name",++qb);b.on("click",function(a){b[0].checked&&c.$setViewValue(d.value,a&&a.type)});c.$render=
function(){b[0].checked=d.value==c.$viewValue};d.$observe("value",c.$render)},checkbox:function(a,b,d,c,e,f,g,h){var k=Ld(h,a,"ngTrueValue",d.ngTrueValue,!0),l=Ld(h,a,"ngFalseValue",d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return na(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:E,button:E,submit:E,reset:E,file:E},Dc=["$browser",
"$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(Vd[N(g.type)]||Vd.text)(e,f,g,h[0],b,a,d,c)}}}}],Jg=/^(true|false|\d+)$/,Se=function(){return{restrict:"A",priority:100,compile:function(a,b){return Jg.test(b.ngValue)?function(a,b,e){e.$set("value",a.$eval(e.ngValue))}:function(a,b,e){a.$watch(e.ngValue,function(a){e.$set("value",a)})}}}},se=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);
return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=z(a)?"":a})}}}}],ue=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=z(a)?"":a})}}}}],te=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=
b(e.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){c.html(a.getTrustedHtml(f(b))||"")})}}}}],Re=da({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),ve=mc("",!0),xe=mc("Odd",0),we=mc("Even",1),ye=Na({compile:function(a,b){b.$set("ngCloak",u);a.removeClass("ng-cloak")}}),ze=[function(){return{restrict:"A",scope:!0,controller:"@",
priority:500}}],Ic={},Kg={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=ya("ng-"+a);Ic[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b],null,!0);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};Kg[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ce=["$animate","$compile",function(a,
b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=ub(h.clone),a.leave(l).then(function(){l=null}),h=null))})}}}],De=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,
transclude:"element",controller:ea.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,p){var q=0,s,x,r,w=function(){x&&(x.remove(),x=null);s&&(s.$destroy(),s=null);r&&(d.leave(r).then(function(){x=null}),x=r,r=null)};c.$watch(f,function(f){var m=function(){!A(h)||h&&!c.$eval(h)||b()},t=++q;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===q){var b=c.$new();n.template=a;a=p(b,function(a){w();d.enter(a,null,e).then(m)});s=b;r=a;s.$emit("$includeContentLoaded",
f);c.$eval(g)}},function(){c.$$destroyed||t!==q||(w(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(w(),n.template=null)})}}}}],Ue=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){ka.call(d[0]).match(/SVG/)?(d.empty(),a(Lc(e.template,P).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],Ee=Na({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),
Qe=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=b.attr(d.$attr.ngList)||", ",f="false"!==d.ngTrim,g=f?W(e):e;c.$parsers.push(function(a){if(!z(a)){var b=[];a&&q(a.split(g),function(a){a&&b.push(f?W(a):a)});return b}});c.$formatters.push(function(a){return M(a)?a.join(e):u});c.$isEmpty=function(a){return!a||!a.length}}}},pb="ng-valid",Md="ng-invalid",Xa="ng-pristine",Lb="ng-dirty",Od="ng-pending",ob=O("ngModel"),Lg=["$scope","$exceptionHandler","$attrs",
"$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,b,d,c,e,f,g,h,k,l){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=u;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=u;this.$name=l(d.name||"",!1)(a);this.$$parentForm=Kb;var m=e(d.ngModel),
n=m.assign,p=m,s=n,y=null,x,r=this;this.$$setOptions=function(a){if((r.$options=a)&&a.getterSetter){var b=e(d.ngModel+"()"),f=e(d.ngModel+"($$$p)");p=function(a){var c=m(a);D(c)&&(c=b(a));return c};s=function(a,b){D(m(a))?f(a,{$$$p:b}):n(a,b)}}else if(!m.assign)throw ob("nonassign",d.ngModel,wa(c));};this.$render=E;this.$isEmpty=function(a){return z(a)||""===a||null===a||a!==a};this.$$updateEmptyClasses=function(a){r.$isEmpty(a)?(f.removeClass(c,"ng-not-empty"),f.addClass(c,"ng-empty")):(f.removeClass(c,
"ng-empty"),f.addClass(c,"ng-not-empty"))};var w=0;Id({ctrl:this,$element:c,set:function(a,b){a[b]=!0},unset:function(a,b){delete a[b]},$animate:f});this.$setPristine=function(){r.$dirty=!1;r.$pristine=!0;f.removeClass(c,Lb);f.addClass(c,Xa)};this.$setDirty=function(){r.$dirty=!0;r.$pristine=!1;f.removeClass(c,Xa);f.addClass(c,Lb);r.$$parentForm.$setDirty()};this.$setUntouched=function(){r.$touched=!1;r.$untouched=!0;f.setClass(c,"ng-untouched","ng-touched")};this.$setTouched=function(){r.$touched=
!0;r.$untouched=!1;f.setClass(c,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){g.cancel(y);r.$viewValue=r.$$lastCommittedViewValue;r.$render()};this.$validate=function(){if(!R(r.$modelValue)||!isNaN(r.$modelValue)){var a=r.$$rawModelValue,b=r.$valid,c=r.$modelValue,d=r.$options&&r.$options.allowInvalid;r.$$runValidators(a,r.$$lastCommittedViewValue,function(e){d||b===e||(r.$modelValue=e?a:u,r.$modelValue!==c&&r.$$writeModelToScope())})}};this.$$runValidators=function(a,b,c){function d(){var c=
!0;q(r.$validators,function(d,e){var g=d(a,b);c=c&&g;f(e,g)});return c?!0:(q(r.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;q(r.$asyncValidators,function(e,g){var h=e(a,b);if(!h||!D(h.then))throw ob("nopromise",h);f(g,u);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?k.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){h===w&&r.$setValidity(a,b)}function g(a){h===w&&c(a)}w++;var h=w;(function(){var a=r.$$parserName||"parse";if(z(x))f(a,null);
else return x||(q(r.$validators,function(a,b){f(b,null)}),q(r.$asyncValidators,function(a,b){f(b,null)})),f(a,x),x;return!0})()?d()?e():g(!1):g(!1)};this.$commitViewValue=function(){var a=r.$viewValue;g.cancel(y);if(r.$$lastCommittedViewValue!==a||""===a&&r.$$hasNativeValidators)r.$$updateEmptyClasses(a),r.$$lastCommittedViewValue=a,r.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var b=r.$$lastCommittedViewValue;if(x=z(b)?u:!0)for(var c=0;c<r.$parsers.length;c++)if(b=
r.$parsers[c](b),z(b)){x=!1;break}R(r.$modelValue)&&isNaN(r.$modelValue)&&(r.$modelValue=p(a));var d=r.$modelValue,e=r.$options&&r.$options.allowInvalid;r.$$rawModelValue=b;e&&(r.$modelValue=b,r.$modelValue!==d&&r.$$writeModelToScope());r.$$runValidators(b,r.$$lastCommittedViewValue,function(a){e||(r.$modelValue=a?b:u,r.$modelValue!==d&&r.$$writeModelToScope())})};this.$$writeModelToScope=function(){s(a,r.$modelValue);q(r.$viewChangeListeners,function(a){try{a()}catch(c){b(c)}})};this.$setViewValue=
function(a,b){r.$viewValue=a;r.$options&&!r.$options.updateOnDefault||r.$$debounceViewValueCommit(b)};this.$$debounceViewValueCommit=function(b){var c=0,d=r.$options;d&&A(d.debounce)&&(d=d.debounce,R(d)?c=d:R(d[b])?c=d[b]:R(d["default"])&&(c=d["default"]));g.cancel(y);c?y=g(function(){r.$commitViewValue()},c):h.$$phase?r.$commitViewValue():a.$apply(function(){r.$commitViewValue()})};a.$watch(function(){var b=p(a);if(b!==r.$modelValue&&(r.$modelValue===r.$modelValue||b===b)){r.$modelValue=r.$$rawModelValue=
b;x=u;for(var c=r.$formatters,d=c.length,e=b;d--;)e=c[d](e);r.$viewValue!==e&&(r.$$updateEmptyClasses(e),r.$viewValue=r.$$lastCommittedViewValue=e,r.$render(),r.$$runValidators(b,e,E))}return b})}],Pe=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:Lg,priority:1,compile:function(b){b.addClass(Xa).addClass("ng-untouched").addClass(pb);return{pre:function(a,b,e,f){var g=f[0];b=f[1]||g.$$parentForm;g.$$setOptions(f[2]&&f[2].$options);b.$addControl(g);
e.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",function(){g.$$parentForm.$removeControl(g)})},post:function(b,c,e,f){var g=f[0];if(g.$options&&g.$options.updateOn)c.on(g.$options.updateOn,function(a){g.$$debounceViewValueCommit(a&&a.type)});c.on("blur",function(){g.$touched||(a.$$phase?b.$evalAsync(g.$setTouched):b.$apply(g.$setTouched))})}}}}}],Mg=/(\s+|^)default(\s+|$)/,Te=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,
b){var d=this;this.$options=pa(a.$eval(b.ngModelOptions));A(this.$options.updateOn)?(this.$options.updateOnDefault=!1,this.$options.updateOn=W(this.$options.updateOn.replace(Mg,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},Fe=Na({terminal:!0,priority:1E3}),Ng=O("ngOptions"),Og=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
Ne=["$compile","$parse",function(a,b){function d(a,c,d){function e(a,b,c,d,f){this.selectValue=a;this.viewValue=b;this.label=c;this.group=d;this.disabled=f}function l(a){var b;if(!p&&za(a))b=a;else{b=[];for(var c in a)a.hasOwnProperty(c)&&"$"!==c.charAt(0)&&b.push(c)}return b}var m=a.match(Og);if(!m)throw Ng("iexp",a,wa(c));var n=m[5]||m[7],p=m[6];a=/ as /.test(m[0])&&m[1];var q=m[9];c=b(m[2]?m[1]:n);var s=a&&b(a)||c,x=q&&b(q),r=q?function(a,b){return x(d,b)}:function(a){return Ha(a)},w=function(a,
b){return r(a,y(a,b))},v=b(m[2]||m[1]),u=b(m[3]||""),t=b(m[4]||""),G=b(m[8]),C={},y=p?function(a,b){C[p]=b;C[n]=a;return C}:function(a){C[n]=a;return C};return{trackBy:q,getTrackByValue:w,getWatchables:b(G,function(a){var b=[];a=a||[];for(var c=l(a),e=c.length,f=0;f<e;f++){var g=a===c?f:c[f],k=a[g],g=y(k,g),k=r(k,g);b.push(k);if(m[2]||m[1])k=v(d,g),b.push(k);m[4]&&(g=t(d,g),b.push(g))}return b}),getOptions:function(){for(var a=[],b={},c=G(d)||[],f=l(c),g=f.length,m=0;m<g;m++){var n=c===f?m:f[m],p=
y(c[n],n),x=s(d,p),n=r(x,p),C=v(d,p),A=u(d,p),p=t(d,p),x=new e(n,x,C,A,p);a.push(x);b[n]=x}return{items:a,selectValueMap:b,getOptionFromViewValue:function(a){return b[w(a)]},getViewValueFromOption:function(a){return q?ea.copy(a.viewValue):a.viewValue}}}}}var c=P.createElement("option"),e=P.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","ngModel"],link:{pre:function(a,b,c,d){d[0].registerOption=E},post:function(b,g,h,k){function l(a,b){a.element=b;b.disabled=a.disabled;
a.label!==b.label&&(b.label=a.label,b.textContent=a.label);a.value!==b.value&&(b.value=a.selectValue)}function m(a,b,c,d){b&&N(b.nodeName)===c?c=b:(c=d.cloneNode(!1),b?a.insertBefore(c,b):a.appendChild(c));return c}function n(a){for(var b;a;)b=a.nextSibling,Yb(a),a=b}function p(a){var b=w&&w[0],c=G&&G[0];if(b||c)for(;a&&(a===b||a===c||8===a.nodeType||"option"===oa(a)&&""===a.value);)a=a.nextSibling;return a}function s(){var a=C&&u.readValue();C=z.getOptions();var b={},d=g[0].firstChild;t&&g.prepend(w);
d=p(d);C.items.forEach(function(a){var f,h;A(a.group)?(f=b[a.group],f||(f=m(g[0],d,"optgroup",e),d=f.nextSibling,f.label=a.group,f=b[a.group]={groupElement:f,currentOptionElement:f.firstChild}),h=m(f.groupElement,f.currentOptionElement,"option",c),l(a,h),f.currentOptionElement=h.nextSibling):(h=m(g[0],d,"option",c),l(a,h),d=h.nextSibling)});Object.keys(b).forEach(function(a){n(b[a].currentOptionElement)});n(d);x.$render();if(!x.$isEmpty(a)){var f=u.readValue();(z.trackBy||r?na(a,f):a===f)||(x.$setViewValue(f),
x.$render())}}var u=k[0],x=k[1],r=h.multiple,w;k=0;for(var v=g.children(),y=v.length;k<y;k++)if(""===v[k].value){w=v.eq(k);break}var t=!!w,G=H(c.cloneNode(!1));G.val("?");var C,z=d(h.ngOptions,g,b);r?(x.$isEmpty=function(a){return!a||0===a.length},u.writeValue=function(a){C.items.forEach(function(a){a.element.selected=!1});a&&a.forEach(function(a){(a=C.getOptionFromViewValue(a))&&!a.disabled&&(a.element.selected=!0)})},u.readValue=function(){var a=g.val()||[],b=[];q(a,function(a){(a=C.selectValueMap[a])&&
!a.disabled&&b.push(C.getViewValueFromOption(a))});return b},z.trackBy&&b.$watchCollection(function(){if(M(x.$viewValue))return x.$viewValue.map(function(a){return z.getTrackByValue(a)})},function(){x.$render()})):(u.writeValue=function(a){var b=C.getOptionFromViewValue(a);b&&!b.disabled?(g[0].value!==b.selectValue&&(G.remove(),t||w.remove(),g[0].value=b.selectValue,b.element.selected=!0),b.element.setAttribute("selected","selected")):null===a||t?(G.remove(),t||g.prepend(w),g.val(""),w.prop("selected",
!0),w.attr("selected",!0)):(t||w.remove(),g.prepend(G),g.val("?"),G.prop("selected",!0),G.attr("selected",!0))},u.readValue=function(){var a=C.selectValueMap[g.val()];return a&&!a.disabled?(t||w.remove(),G.remove(),C.getViewValueFromOption(a)):null},z.trackBy&&b.$watch(function(){return z.getTrackByValue(x.$viewValue)},function(){x.$render()}));t?(w.remove(),a(w)(b),w.removeClass("ng-scope")):w=H(c.cloneNode(!1));s();b.$watchCollection(z.getWatchables,s)}}}}],Ge=["$locale","$interpolate","$log",function(a,
b,d){var c=/{}/g,e=/^when(Minus)?(.+)$/;return{link:function(f,g,h){function k(a){g.text(a||"")}var l=h.count,m=h.$attr.when&&g.attr(h.$attr.when),n=h.offset||0,p=f.$eval(m)||{},s={},u=b.startSymbol(),x=b.endSymbol(),r=u+l+"-"+n+x,w=ea.noop,v;q(h,function(a,b){var c=e.exec(b);c&&(c=(c[1]?"-":"")+N(c[2]),p[c]=g.attr(h.$attr[b]))});q(p,function(a,d){s[d]=b(a.replace(c,r))});f.$watch(l,function(b){var c=parseFloat(b),e=isNaN(c);e||c in p||(c=a.pluralCat(c-n));c===v||e&&R(v)&&isNaN(v)||(w(),e=s[c],z(e)?
(null!=b&&d.debug("ngPluralize: no rule defined for '"+c+"' in "+m),w=E,k()):w=f.$watch(e,k),v=c)})}}}],He=["$parse","$animate","$compile",function(a,b,d){var c=O("ngRepeat"),e=function(a,b,c,d,e,m,n){a[c]=d;e&&(a[e]=m);a.$index=b;a.$first=0===b;a.$last=b===n-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(b&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,k=d.$$createComment("end ngRepeat",h),l=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if(!l)throw c("iexp",h);var m=l[1],n=l[2],p=l[3],s=l[4],l=m.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!l)throw c("iidexp",m);var y=l[3]||l[1],x=l[2];if(p&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(p)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(p)))throw c("badident",p);var r,w,v,z,t={$id:Ha};s?r=a(s):(v=function(a,b){return Ha(b)},z=function(a){return a});return function(a,d,f,g,l){r&&(w=function(b,c,d){x&&(t[x]=b);t[y]=c;t.$index=
d;return r(a,t)});var m=V();a.$watchCollection(n,function(f){var g,n,r=d[0],s,t=V(),A,E,H,D,I,F,J;p&&(a[p]=f);if(za(f))I=f,n=w||v;else for(J in n=w||z,I=[],f)va.call(f,J)&&"$"!==J.charAt(0)&&I.push(J);A=I.length;J=Array(A);for(g=0;g<A;g++)if(E=f===I?g:I[g],H=f[E],D=n(E,H,g),m[D])F=m[D],delete m[D],t[D]=F,J[g]=F;else{if(t[D])throw q(J,function(a){a&&a.scope&&(m[a.id]=a)}),c("dupes",h,D,H);J[g]={id:D,scope:u,clone:u};t[D]=!0}for(s in m){F=m[s];D=ub(F.clone);b.leave(D);if(D[0].parentNode)for(g=0,n=D.length;g<
n;g++)D[g].$$NG_REMOVED=!0;F.scope.$destroy()}for(g=0;g<A;g++)if(E=f===I?g:I[g],H=f[E],F=J[g],F.scope){s=r;do s=s.nextSibling;while(s&&s.$$NG_REMOVED);F.clone[0]!=s&&b.move(ub(F.clone),null,r);r=F.clone[F.clone.length-1];e(F.scope,g,y,H,x,E,A)}else l(function(a,c){F.scope=c;var d=k.cloneNode(!1);a[a.length++]=d;b.enter(a,null,r);r=d;F.clone=a;t[F.id]=F;e(F.scope,g,y,H,x,E,A)});m=t})}}}}],Ie=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngShow,function(b){a[b?
"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Be=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(b,d,c){b.$watch(c.ngHide,function(b){a[b?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Je=Na(function(a,b,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,c){b.css(c,"")});a&&b.css(a)},!0)}),Ke=["$animate","$compile",function(a,b){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],
link:function(d,c,e,f){var g=[],h=[],k=[],l=[],m=function(a,b){return function(){a.splice(b,1)}};d.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=k.length;d<e;++d)a.cancel(k[d]);d=k.length=0;for(e=l.length;d<e;++d){var s=ub(h[d].clone);l[d].$destroy();(k[d]=a.leave(s)).then(m(k,d))}h.length=0;l.length=0;(g=f.cases["!"+c]||f.cases["?"])&&q(g,function(c){c.transclude(function(d,e){l.push(e);var f=c.element;d[d.length++]=b.$$createComment("end ngSwitchWhen");h.push({clone:d});a.enter(d,f.parent(),
f)})})})}}}],Le=Na({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["!"+d.ngSwitchWhen]=c.cases["!"+d.ngSwitchWhen]||[];c.cases["!"+d.ngSwitchWhen].push({transclude:e,element:b})}}),Me=Na({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,b,d,c,e){c.cases["?"]=c.cases["?"]||[];c.cases["?"].push({transclude:e,element:b})}}),Pg=O("ngTransclude"),Oe=Na({restrict:"EAC",link:function(a,b,d,c,e){d.ngTransclude===
d.$attr.ngTransclude&&(d.ngTransclude="");if(!e)throw Pg("orphan",wa(b));e(function(a){a.length&&(b.empty(),b.append(a))},null,d.ngTransclude||d.ngTranscludeSlot)}}),oe=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(b,d){"text/ng-template"==d.type&&a.put(d.id,b[0].text)}}}],Qg={$setViewValue:E,$render:E},Rg=["$element","$scope",function(a,b){var d=this,c=new Ua;d.ngModelCtrl=Qg;d.unknownOption=H(P.createElement("option"));d.renderUnknownOption=function(b){b="? "+Ha(b)+
" ?";d.unknownOption.val(b);a.prepend(d.unknownOption);a.val(b)};b.$on("$destroy",function(){d.renderUnknownOption=E});d.removeUnknownOption=function(){d.unknownOption.parent()&&d.unknownOption.remove()};d.readValue=function(){d.removeUnknownOption();return a.val()};d.writeValue=function(b){d.hasOption(b)?(d.removeUnknownOption(),a.val(b),""===b&&d.emptyOption.prop("selected",!0)):null==b&&d.emptyOption?(d.removeUnknownOption(),a.val("")):d.renderUnknownOption(b)};d.addOption=function(a,b){if(8!==
b[0].nodeType){Ta(a,'"option value"');""===a&&(d.emptyOption=b);var g=c.get(a)||0;c.put(a,g+1);d.ngModelCtrl.$render();b[0].hasAttribute("selected")&&(b[0].selected=!0)}};d.removeOption=function(a){var b=c.get(a);b&&(1===b?(c.remove(a),""===a&&(d.emptyOption=u)):c.put(a,b-1))};d.hasOption=function(a){return!!c.get(a)};d.registerOption=function(a,b,c,h,k){if(h){var l;c.$observe("value",function(a){A(l)&&d.removeOption(l);l=a;d.addOption(a,b)})}else k?a.$watch(k,function(a,e){c.$set("value",a);e!==
a&&d.removeOption(e);d.addOption(a,b)}):d.addOption(c.value,b);b.on("$destroy",function(){d.removeOption(c.value);d.ngModelCtrl.$render()})}}],pe=function(){return{restrict:"E",require:["select","?ngModel"],controller:Rg,priority:1,link:{pre:function(a,b,d,c){var e=c[1];if(e){var f=c[0];f.ngModelCtrl=e;b.on("change",function(){a.$apply(function(){e.$setViewValue(f.readValue())})});if(d.multiple){f.readValue=function(){var a=[];q(b.find("option"),function(b){b.selected&&a.push(b.value)});return a};
f.writeValue=function(a){var c=new Ua(a);q(b.find("option"),function(a){a.selected=A(c.get(a.value))})};var g,h=NaN;a.$watch(function(){h!==e.$viewValue||na(g,e.$viewValue)||(g=ia(e.$viewValue),e.$render());h=e.$viewValue});e.$isEmpty=function(a){return!a||0===a.length}}}},post:function(a,b,d,c){var e=c[1];if(e){var f=c[0];e.$render=function(){f.writeValue(e.$viewValue)}}}}}},re=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(b,d){if(A(d.value))var c=a(d.value,!0);else{var e=
a(b.text(),!0);e||d.$set("value",b.text())}return function(a,b,d){var k=b.parent();(k=k.data("$selectController")||k.parent().data("$selectController"))&&k.registerOption(a,b,d,c,e)}}}}],qe=da({restrict:"E",terminal:!1}),Fc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){c&&(d.required=!0,c.$validators.required=function(a,b){return!d.required||!c.$isEmpty(b)},d.$observe("required",function(){c.$validate()}))}}},Ec=function(){return{restrict:"A",require:"?ngModel",link:function(a,
b,d,c){if(c){var e,f=d.ngPattern||d.pattern;d.$observe("pattern",function(a){y(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw O("ngPattern")("noregexp",f,a,wa(b));e=a||u;c.$validate()});c.$validators.pattern=function(a,b){return c.$isEmpty(b)||z(e)||e.test(b)}}}}},Hc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=-1;d.$observe("maxlength",function(a){a=Y(a);e=isNaN(a)?-1:a;c.$validate()});c.$validators.maxlength=function(a,b){return 0>e||c.$isEmpty(b)||
b.length<=e}}}}},Gc=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Y(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};T.angular.bootstrap?T.console&&console.log("WARNING: Tried to load angular more than once."):(he(),je(ea),ea.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM",
"PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5,
6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a,
c){var e=a|0,f=c;u===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),H(P).ready(function(){de(P,yc)}))})(window,document);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
//# sourceMappingURL=angular.min.js.map

查看文件

@ -1,45 +0,0 @@
"use strict";
var lab = angular.module('lab', []);
lab.controller('LabCtrl', function ($scope, $http, $timeout) {
$scope.noun1 = "";
$scope.noun2 = "";
$scope.adjective1 = "";
$scope.adjective2 = "";
$scope.verb = "";
getWord($http, $timeout, '/words/noun', function(resp) {
$scope.noun1 = word(resp);
});
getWord($http, $timeout, '/words/noun', function(resp) {
$scope.noun2 = word(resp);
});
getWord($http, $timeout, '/words/adjective', function(resp) {
var adj = word(resp);
adj.word = adj.word.charAt(0).toUpperCase() + adj.word.substr(1)
$scope.adjective1 = adj;
});
getWord($http, $timeout, '/words/adjective', function(resp) {
$scope.adjective2 = word(resp);
});
getWord($http, $timeout, '/words/verb', function(resp) {
$scope.verb = word(resp);
});
});
function getWord($http, $timeout, url, callback) {
$http.get(url).then(callback, function(resp) {
$timeout(function() {
console.log("Retry: " + url);
getWord($http, $timeout, url, callback);
}, 500);
});
}
function word(resp) {
return {
word: resp.data.word,
hostname: resp.headers()["source"]
};
}

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 15 KiB

二进制文件未显示。

二进制文件未显示。

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 14 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 101 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 85 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 1.5 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 1.2 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 1.2 KiB

查看文件

@ -1,48 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.398 30">
<defs>
<style>
.cls-1, .cls-2 {
fill: #fff;
}
.cls-2 {
fill-rule: evenodd;
}
.cls-3 {
fill: #ffcd34;
}
</style>
</defs>
<g id="Symbol_13_1" data-name="Symbol 13 – 1" transform="translate(-525.047 1426)">
<g id="Group_7165" data-name="Group 7165" transform="translate(525.047 -1426)">
<g id="Group_7138" data-name="Group 7138" transform="translate(212.832 15.659)">
<path id="Path_16931" data-name="Path 16931" class="cls-1" d="M838.243,526.229a.311.311,0,0,1-.233.543h-3.1a.274.274,0,0,1-.31-.31V521.81a.274.274,0,0,1,.31-.31h3.1a.311.311,0,1,1,0,.62H835.3v1.628h1.938a.311.311,0,1,1,0,.62H835.3v1.705h2.713C838.088,526.151,838.166,526.151,838.243,526.229Z" transform="translate(-834.6 -521.5)"/>
<path id="Path_16932" data-name="Path 16932" class="cls-1" d="M840.133,521.578a.311.311,0,0,1,.543.233v3.178a.88.88,0,0,0,.233.62,3.369,3.369,0,0,0,.543.465,2.015,2.015,0,0,0,1.55,0,1.042,1.042,0,0,0,.543-.465.835.835,0,0,0,.155-.62V521.81a.311.311,0,1,1,.62,0v3.178a1.558,1.558,0,0,1-.31,1.008,1.7,1.7,0,0,1-.775.7,2.476,2.476,0,0,1-1.085.233,2.589,2.589,0,0,1-1.163-.233,1.7,1.7,0,0,1-.775-.7,2.2,2.2,0,0,1-.31-1.008V521.81C839.977,521.733,840.055,521.578,840.133,521.578Z" transform="translate(-835.791 -521.5)"/>
</g>
<g id="Group_7143" data-name="Group 7143">
<g id="Group_7140" data-name="Group 7140">
<g id="Group_7139" data-name="Group 7139" transform="translate(44.072 3.256)">
<path id="Path_16933" data-name="Path 16933" class="cls-1" d="M810.8,517.273a7.184,7.184,0,0,1,4.884-1.473,8.293,8.293,0,0,1,3.333.7,9.763,9.763,0,0,1,2.791,1.86,8.8,8.8,0,0,1,1.86,2.791,8.391,8.391,0,0,1,.7,3.411v8.217a1.542,1.542,0,0,1-.543,1.24,2,2,0,0,1-1.24.543,1.542,1.542,0,0,1-1.24-.543,1.829,1.829,0,0,1-.543-1.24v-8.295a5.823,5.823,0,0,0-.388-2.016,5.974,5.974,0,0,0-1.085-1.628,4.619,4.619,0,0,0-1.628-1.085,4.46,4.46,0,0,0-2.016-.388,5.823,5.823,0,0,0-2.016.388,5.974,5.974,0,0,0-1.628,1.085,4.619,4.619,0,0,0-1.085,1.628,4.459,4.459,0,0,0-.388,2.016V532.7a1.542,1.542,0,0,1-.543,1.24,2,2,0,0,1-1.24.543,1.542,1.542,0,0,1-1.24-.543A2,2,0,0,1,807,532.7V517.66a2.2,2.2,0,0,1,.543-1.318,1.542,1.542,0,0,1,1.24-.543,1.651,1.651,0,0,1,1.24.543A9.848,9.848,0,0,1,810.8,517.273Z" transform="translate(-659.636 -507.815)"/>
<path id="Path_16934" data-name="Path 16934" class="cls-1" d="M764.216,521.016a6.014,6.014,0,0,1,1.86-1.24,5.561,5.561,0,0,1,2.248-.465,5.823,5.823,0,0,1,2.015.388,7.333,7.333,0,0,1,1.783,1.085,1.852,1.852,0,0,0,1.085.388,1.707,1.707,0,0,0,1.085-3.023,8.791,8.791,0,0,0-5.969-2.248,9.225,9.225,0,0,0,0,18.45,8.791,8.791,0,0,0,5.969-2.248,1.652,1.652,0,0,0,.543-1.24,1.76,1.76,0,0,0-.465-1.24,1.636,1.636,0,0,0-1.24-.465,1.852,1.852,0,0,0-1.085.388,5.772,5.772,0,0,1-1.705,1.085,5.49,5.49,0,0,1-2.015.388,5.561,5.561,0,0,1-2.248-.465,5.7,5.7,0,0,1-3.1-3.1,5.666,5.666,0,0,1,0-4.5A4.416,4.416,0,0,1,764.216,521.016Z" transform="translate(-648.867 -507.838)"/>
<path id="Path_16935" data-name="Path 16935" class="cls-1" d="M658.836,518.513a9.209,9.209,0,1,0-6.512,15.736,9.155,9.155,0,0,0,6.512-2.713,8.9,8.9,0,0,0,2.713-6.512,9.14,9.14,0,0,0-.7-3.566A11.732,11.732,0,0,0,658.836,518.513Zm-1.24,8.837a5.7,5.7,0,0,1-3.1,3.1,5.666,5.666,0,0,1-4.5,0,4.917,4.917,0,0,1-1.783-1.24,6.014,6.014,0,0,1-1.24-1.86,5.666,5.666,0,0,1,0-4.5,6.016,6.016,0,0,1,1.24-1.86,5.678,5.678,0,0,1,1.783-1.24,5.665,5.665,0,0,1,4.5,0,5.7,5.7,0,0,1,3.1,3.1,5.665,5.665,0,0,1,0,4.5Z" transform="translate(-622.79 -507.815)"/>
<path id="Path_16936" data-name="Path 16936" class="cls-1" d="M633.567,505.6a1.76,1.76,0,0,0-1.24.465,1.637,1.637,0,0,0-.465,1.24v8.217a9.212,9.212,0,1,0-5.736,16.434,9.155,9.155,0,0,0,6.512-2.713,8.9,8.9,0,0,0,2.713-6.512v-15.5a1.76,1.76,0,0,0-.465-1.24A1.792,1.792,0,0,0,633.567,505.6Zm-2.093,19.457a5.7,5.7,0,0,1-3.1,3.1,5.665,5.665,0,0,1-4.5,0,4.917,4.917,0,0,1-1.783-1.24,6.014,6.014,0,0,1-1.24-1.86,5.666,5.666,0,0,1,0-4.5,6.015,6.015,0,0,1,1.24-1.86,5.679,5.679,0,0,1,1.783-1.24,5.666,5.666,0,0,1,4.5,0,5.7,5.7,0,0,1,3.1,3.1,5.56,5.56,0,0,1,.465,2.248A10.513,10.513,0,0,1,631.474,525.057Z" transform="translate(-616.9 -505.522)"/>
<path id="Path_16937" data-name="Path 16937" class="cls-1" d="M754.621,518.823a2.263,2.263,0,0,0,.155-.7,1.578,1.578,0,0,0-.465-1.163,2.92,2.92,0,0,0-1.163-.7,9.065,9.065,0,0,0-1.55-.388,10.917,10.917,0,0,0-1.55-.078,8.433,8.433,0,0,0-3.1.543,9.075,9.075,0,0,0-2.636,1.55v-.31a1.9,1.9,0,0,0-.465-1.24,1.687,1.687,0,0,0-1.24-.543,1.651,1.651,0,0,0-1.24.543,1.76,1.76,0,0,0-.465,1.24v15.039a1.9,1.9,0,0,0,.465,1.24,1.829,1.829,0,0,0,1.24.543,1.652,1.652,0,0,0,1.24-.543,1.761,1.761,0,0,0,.465-1.24V525.1a5.561,5.561,0,0,1,.465-2.248,5.7,5.7,0,0,1,3.1-3.1,5.56,5.56,0,0,1,2.248-.465,7.2,7.2,0,0,1,2.248.388,1.416,1.416,0,0,0,.7.155,2.265,2.265,0,0,0,.7-.155,1.39,1.39,0,0,0,.543-.388A.712.712,0,0,0,754.621,518.823Z" transform="translate(-644.776 -507.815)"/>
<path id="Path_16938" data-name="Path 16938" class="cls-1" d="M730.536,518.513a9.209,9.209,0,1,0-6.512,15.736A8.791,8.791,0,0,0,729.994,532a1.886,1.886,0,0,0,0-2.481,1.637,1.637,0,0,0-1.24-.465,1.862,1.862,0,0,0-1.163.465,5.308,5.308,0,0,1-3.721,1.318,5.211,5.211,0,0,1-1.86-.31,6.528,6.528,0,0,1-1.628-.853,5.022,5.022,0,0,1-1.24-1.318,6.528,6.528,0,0,1-.853-1.628h12.946a1.9,1.9,0,0,0,1.24-.465,1.637,1.637,0,0,0,.465-1.24,9.14,9.14,0,0,0-.7-3.566A6.108,6.108,0,0,0,730.536,518.513ZM718.521,523.4a5.277,5.277,0,0,1,.775-1.628,9.979,9.979,0,0,1,1.24-1.318,5.413,5.413,0,0,1,1.628-.853,4.806,4.806,0,0,1,1.783-.31,4.547,4.547,0,0,1,1.783.31,6.525,6.525,0,0,1,1.628.853,5.021,5.021,0,0,1,1.24,1.318,6.527,6.527,0,0,1,.853,1.628Z" transform="translate(-638.909 -507.815)"/>
<path id="Path_16939" data-name="Path 16939" class="cls-1" d="M796.436,518.513a9.209,9.209,0,1,0-6.512,15.736,9.155,9.155,0,0,0,6.512-2.713,8.9,8.9,0,0,0,2.713-6.512,9.139,9.139,0,0,0-.7-3.566A11.734,11.734,0,0,0,796.436,518.513Zm-1.163,8.837a5.7,5.7,0,0,1-3.1,3.1,5.666,5.666,0,0,1-4.5,0,4.916,4.916,0,0,1-1.783-1.24,6.013,6.013,0,0,1-1.24-1.86,5.666,5.666,0,0,1,0-4.5,6.017,6.017,0,0,1,1.24-1.86,5.676,5.676,0,0,1,1.783-1.24,5.665,5.665,0,0,1,4.5,0,5.7,5.7,0,0,1,3.1,3.1,5.561,5.561,0,0,1,.465,2.248A10.516,10.516,0,0,1,795.274,527.35Z" transform="translate(-653.723 -507.815)"/>
<path id="Path_16940" data-name="Path 16940" class="cls-1" d="M707.891,515.267a2.264,2.264,0,0,0-.155-.7,2.027,2.027,0,0,0-.93-.93,2.263,2.263,0,0,0-.7-.155,2.065,2.065,0,0,0-.93.233l-9.767,6.434V507.283a1.761,1.761,0,0,0-.465-1.24,1.687,1.687,0,0,0-1.24-.543,1.651,1.651,0,0,0-1.24.543,1.76,1.76,0,0,0-.465,1.24v22.946a1.9,1.9,0,0,0,.465,1.24,1.829,1.829,0,0,0,1.24.543,1.652,1.652,0,0,0,1.24-.543,1.761,1.761,0,0,0,.465-1.24V524.26l2.015-1.318,7.6,8.6a1.578,1.578,0,0,0,1.163.465,2.263,2.263,0,0,0,.7-.155,2.028,2.028,0,0,0,.93-.93,2.264,2.264,0,0,0,.155-.7,1.9,1.9,0,0,0-.465-1.24L700.45,521l6.822-4.5A1.408,1.408,0,0,0,707.891,515.267Z" transform="translate(-633.783 -505.5)"/>
<path id="Path_16941" data-name="Path 16941" class="cls-1" d="M674.416,521.016a6.014,6.014,0,0,1,1.86-1.24,5.561,5.561,0,0,1,2.248-.465,5.823,5.823,0,0,1,2.016.388,7.333,7.333,0,0,1,1.783,1.085,1.852,1.852,0,0,0,1.085.388,1.707,1.707,0,0,0,1.085-3.023,8.791,8.791,0,0,0-5.969-2.248,9.225,9.225,0,0,0,0,18.45,8.791,8.791,0,0,0,5.969-2.248,1.651,1.651,0,0,0,.543-1.24,1.76,1.76,0,0,0-.465-1.24,1.636,1.636,0,0,0-1.24-.465,1.852,1.852,0,0,0-1.085.388,5.772,5.772,0,0,1-1.705,1.085,5.49,5.49,0,0,1-2.016.388,5.561,5.561,0,0,1-2.248-.465,5.7,5.7,0,0,1-3.1-3.1,5.666,5.666,0,0,1,0-4.5A3.832,3.832,0,0,1,674.416,521.016Z" transform="translate(-628.68 -507.838)"/>
</g>
<path id="Path_16942" data-name="Path 16942" class="cls-2" d="M584.352,515.021h4.419V510.99h-4.419v4.031Zm-5.271,0H583.5V510.99H579.08v4.031Zm-5.271,0h4.419V510.99h-4.419v4.031Zm-5.194,0h4.419V510.99h-4.419v4.031Zm-5.271,0h4.419V510.99h-4.419v4.031Zm5.271-4.884h4.419v-4.031h-4.419v4.031Zm5.194,0h4.419v-4.031h-4.419v4.031Zm5.271,0H583.5v-4.031H579.08v4.031Zm0-4.806H583.5V501.3H579.08v4.031Zm23.023,7.287a8.078,8.078,0,0,0-4.884-.543,6.235,6.235,0,0,0-2.791-4.264l-.93-.62-.62.93a7.348,7.348,0,0,0-1.085,4.5,5.857,5.857,0,0,0,.853,2.481,6.331,6.331,0,0,1-3.256.7H560.243l-.077.31a14.194,14.194,0,0,0,3.023,10.543c2.481,2.946,6.2,4.419,11.085,4.419,10.543,0,18.372-4.884,22.016-13.721,1.473,0,4.574,0,6.124-3.023a2.828,2.828,0,0,0,.388-.853l.155-.31-.853-.543Z" transform="translate(-560.047 -501.3)"/>
</g>
<g id="Group_7142" data-name="Group 7142" transform="translate(210.584 5.194)">
<path id="Path_16943" data-name="Path 16943" class="cls-3" d="M859.966,521.411a6.94,6.94,0,0,1,.853,3.566,8.178,8.178,0,0,1-.93,3.876,6.976,6.976,0,0,1-6.2,3.721,7.072,7.072,0,0,1-3.721-1.008,7.387,7.387,0,0,1-2.636-2.713,8.054,8.054,0,0,1-.078-7.364,6.558,6.558,0,0,1,2.481-2.558,6.077,6.077,0,0,1-1.705-2.016,6.26,6.26,0,0,1-.62-2.636,5.6,5.6,0,0,1,.853-3.178,6.253,6.253,0,0,1,5.426-3.1,5.949,5.949,0,0,1,3.1.775,5.832,5.832,0,0,1,2.171,2.171,6.233,6.233,0,0,1,.775,3.178,6.336,6.336,0,0,1-.62,2.713,5.4,5.4,0,0,1-1.705,2.016A6.744,6.744,0,0,1,859.966,521.411Zm-3.333.543a3.7,3.7,0,0,0-2.946-1.24,3.813,3.813,0,0,0-2.946,1.24,4.593,4.593,0,0,0-1.163,3.178,4.682,4.682,0,0,0,.543,2.326,4.29,4.29,0,0,0,1.473,1.628,3.842,3.842,0,0,0,4.186,0,4.538,4.538,0,0,0,1.473-1.705,4.785,4.785,0,0,0,.543-2.4A4.215,4.215,0,0,0,856.633,521.953Zm-5.116-5.349a3.014,3.014,0,0,0,2.171,1.008,2.644,2.644,0,0,0,2.171-1.008,3.464,3.464,0,0,0,.853-2.481,3.1,3.1,0,0,0-.853-2.248,2.933,2.933,0,0,0-2.171-.93,2.982,2.982,0,0,0-2.248.93,3.1,3.1,0,0,0-.853,2.326A3.466,3.466,0,0,0,851.516,516.6Z" transform="translate(-835.005 -508)"/>
<g id="Group_7141" data-name="Group 7141" transform="translate(0 0.078)">
<path id="Path_16944" data-name="Path 16944" class="cls-3" d="M838.1,530.2v5.814a1.578,1.578,0,0,0,.465,1.163,1.53,1.53,0,0,0,1.085.465,1.59,1.59,0,0,0,1.628-1.628V530.2Z" transform="translate(-833.139 -513.068)"/>
<path id="Path_16945" data-name="Path 16945" class="cls-3" d="M833.328,513.139a2.675,2.675,0,0,0,1.008-.233l2.326-1.008v5.194h3.178v-7.364a1.59,1.59,0,0,0-1.628-1.628,3.486,3.486,0,0,0-.62.155l-4.884,1.783a1.279,1.279,0,0,0-.775.62,1.653,1.653,0,0,0-.233.93,1.578,1.578,0,0,0,.465,1.163A1.9,1.9,0,0,0,833.328,513.139Z" transform="translate(-831.7 -508.1)"/>
</g>
</g>
</g>
</g>
</g>
</svg>

之前

宽度:  |  高度:  |  大小: 10 KiB

查看文件

@ -1,45 +0,0 @@
<!DOCTYPE html>
<html lang="en" ng-app="lab">
<head>
<meta charset="utf-8">
<title>Docker Compose demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="header logo"><img src="images/dockercon-logo-2020.png" /></div>
<div class="sentence" ng-controller="LabCtrl">
<div class="line line1 slide-in">
<span class="result adjective slide-in">
<span class="word slide-in" ng-bind="adjective1.word"></span>
<span class="hostname" ng-bind="adjective1.hostname"></span>
</span>
<span class="result noun slide-in">
<span class="word" ng-bind="noun1.word"></span>
<span class="hostname" ng-bind="noun1.hostname"></span>
</span>
</div>
<div class="line line2 slide-in">
<span class="result verb slide-in">
<span class="word" ng-bind="verb.word"></span>
<span class="hostname" ng-bind="verb.hostname"></span>
</span>
</div>
<div class="line line3 slide-in">
<span class="result adjective slide-in">
<span class="word" ng-bind="adjective2.word"></span>
<span class="hostname" ng-bind="adjective2.hostname"></span>
</span>
<span class="result noun slide-in">
<span class="word" ng-bind="noun2.word"></span>
<span class="hostname" ng-bind="noun2.hostname"></span>
</span>
</div>
</div>
<div class="footer"><img src="images/homes.png" /></div>
</body>
<script src="angular.min.js"></script>
<script src="app.js"></script>
</html>

查看文件

@ -1,110 +0,0 @@
/* latin-ext */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
src: local('Raleway'), local('Raleway-Regular'), url('fonts/font1.woff2') format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
src: local('Raleway'), local('Raleway-Regular'), url('fonts/font2.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
body {
text-align: center;
margin: 0;
padding: 0;
background-color: #001f5b;
}
.logo img{
margin: -50px 0;
width: 435px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
z-index: -1;
opacity: 0.5;
}
.footer img {
max-width: 100%;
vertical-align: middle;
}
.sentence {
margin: 70px auto 0 auto;
}
.line {
margin-bottom: 30px;
transform: translateX(-100%) rotate(-20deg);
}
.slide-in {
animation: slide-in .5s forwards ease-in;
}
.line3.slide-in {
animation: slide-in 1s forwards ease-in;
}
.line2.slide-in {
animation: slide-in 1.2s forwards ease-in;
}
@keyframes slide-in {
100% {
transform: translateX(0%);
}
}
.result {
position: relative;
display: inline-block;
padding: 0 20px;
margin: 0 10px;
color: white;
height: 175px;
width: 330px;
}
.result .word {
display: inline-block;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 45px;
color: white;
line-height: 155px;
height: 175px;
vertical-align: middle;
margin-top: 20px;
}
.result .hostname {
position: absolute;
width: 100%;
left: 0;
bottom: 8px;
font-size: 0.8em;
height: 14px;
}
.noun {
background-image: url('images/lego_blue.png') !important;
}
.verb {
background-image: url('images/lego_yellow.png') !important;
}
.adjective {
background-image: url('images/lego_light_blue.png') !important;
}

查看文件

@ -1,3 +0,0 @@
.idea
target
*.iml

查看文件

@ -1,38 +0,0 @@
# Copyright 2020 Docker Compose CLI 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.
# BUILD
FROM openjdk:8u171-jdk-alpine as build
RUN MAVEN_VERSION=3.5.0 \
&& cd /usr/share \
&& wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz -O - | tar xzf - \
&& mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
WORKDIR /home/lab
COPY pom.xml .
RUN mvn verify -DskipTests --fail-never
COPY src ./src
RUN mvn verify
# RUN
FROM openjdk:8u171-jre-alpine
ENTRYPOINT ["java", "-Xmx8m", "-Xms8m", "-jar", "/app/words.jar"]
EXPOSE 8080
WORKDIR /app
COPY --from=build /home/lab/target .

查看文件

@ -1,99 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codestory</groupId>
<artifactId>words</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<finalName>words</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.6</version>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>dependency</classpathPrefix>
<mainClass>Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>[24.1.1,)</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.1.4</version>
</dependency>
</dependencies>
</project>

查看文件

@ -1,53 +0,0 @@
import com.google.common.base.Charsets;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.sql.*;
import java.util.NoSuchElementException;
public class Main {
public static void main(String[] args) throws Exception {
Class.forName("org.postgresql.Driver");
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/noun", handler(Suppliers.memoize(() -> randomWord("nouns"))));
server.createContext("/verb", handler(Suppliers.memoize(() -> randomWord("verbs"))));
server.createContext("/adjective", handler(Suppliers.memoize(() -> randomWord("adjectives"))));
server.start();
}
private static String randomWord(String table) {
try (Connection connection = DriverManager.getConnection("jdbc:postgresql://db:5432/postgres", "postgres", "")) {
try (Statement statement = connection.createStatement()) {
try (ResultSet set = statement.executeQuery("SELECT word FROM " + table + " ORDER BY random() LIMIT 1")) {
while (set.next()) {
return set.getString(1);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
throw new NoSuchElementException(table);
}
private static HttpHandler handler(Supplier<String> word) {
return t -> {
String response = "{\"word\":\"" + word.get() + "\"}";
byte[] bytes = response.getBytes(Charsets.UTF_8);
System.out.println(response);
t.getResponseHeaders().add("content-type", "application/json; charset=utf-8");
t.sendResponseHeaders(200, bytes.length);
try (OutputStream os = t.getResponseBody()) {
os.write(bytes);
}
};
}
}

查看文件

@ -1,46 +0,0 @@
services:
web1:
build: ./web
image: dockerinternal/e2e_test_secret_server
ports:
- "80:80"
secrets:
- source: mysecret1
target: mytarget1
- mysecret2
deploy:
restart_policy:
condition: on-failure
resources:
limits:
cpus: '0.7'
memory: 1G
reservations:
cpus: '0.5'
memory: 0.5G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/healthz"]
interval: 5s
web2:
build: ./web
image: dockerinternal/e2e_test_secret_server
ports:
- "8080:8080"
environment:
- PORT=8080
deploy:
restart_policy:
condition: on-failure
resources:
reservations:
cpus: '0.5'
memory: 0.7G
secrets:
- mysecret2
secrets:
mysecret1:
file: ./my_secret1.txt
mysecret2:
file: ./my_secret2.txt

查看文件

@ -1 +0,0 @@
myPassword1

查看文件

@ -1 +0,0 @@
another_password

查看文件

@ -1,26 +0,0 @@
# syntax=docker/dockerfile:experimental
# Copyright 2020 Docker Compose CLI 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.
FROM golang:1.16-alpine AS build
COPY main.go .
RUN --mount=type=cache,target=/go/pkg/mod \
go build -trimpath -ldflags="-s -w" -o server main.go
FROM alpine
RUN apk --no-cache add curl
COPY --from=build /go/server /
CMD /server "${PORT:-80}" "${DIR:-/run/secrets}"

查看文件

@ -1,65 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 (
"fmt"
"net/http"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: web PORT FOLDER")
os.Exit(1)
}
http.HandleFunc("/failtestserver", log(fail))
http.HandleFunc("/healthz", log(healthz))
dir := os.Args[2]
fileServer := http.FileServer(http.Dir(dir))
http.HandleFunc("/", log(fileServer.ServeHTTP))
port := os.Args[1]
fmt.Println("Listening on port " + port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
fmt.Printf("Error while starting server: %v", err)
}
}
var healthy = true
func fail(w http.ResponseWriter, req *http.Request) {
healthy = false
fmt.Println("Server failing")
}
func healthz(w http.ResponseWriter, r *http.Request) {
if !healthy {
fmt.Println("unhealthy")
w.WriteHeader(http.StatusServiceUnavailable)
return
}
}
func log(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
handler(w, r)
}
}

文件差异内容过多而无法显示 加载差异

查看文件

@ -1,21 +0,0 @@
# Copyright 2020 Docker Compose CLI 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.
FROM golang:1.16 AS builder
WORKDIR $GOPATH/src/github.com/docker/compose-cli/aci/etchosts
COPY . .
RUN GO111MODULE=auto CGO_ENABLED=0 go build -ldflags="-w -s" -o /go/bin/hosts main/main.go
FROM scratch
COPY --from=builder /go/bin/hosts /hosts

查看文件

@ -1,42 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 etchosts
import (
"fmt"
"os"
"strings"
)
// SetHostNames appends hosts aliases for loopback address to etc/host file
func SetHostNames(file string, hosts ...string) error {
f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close() //nolint:errcheck
fmt.Println("Setting local hosts for " + strings.Join(hosts, ", "))
for _, host := range hosts {
_, err = f.WriteString("\n127.0.0.1 " + host)
if err != nil {
return err
}
}
_, err = f.WriteString("\n")
return err
}

查看文件

@ -1,48 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 etchosts
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/fs"
"gotest.tools/v3/golden"
)
func TestSetDomain(t *testing.T) {
dir := fs.NewDir(t, "resolv").Path()
f := filepath.Join(dir, "hosts")
touch(t, f)
err := SetHostNames(f, "foo", "bar", "zot")
assert.NilError(t, err)
got, err := ioutil.ReadFile(f)
assert.NilError(t, err)
golden.Assert(t, string(got), "etchosts.golden")
}
func touch(t *testing.T, f string) {
file, err := os.Create(f)
assert.NilError(t, err)
err = file.Close()
assert.NilError(t, err)
}

查看文件

@ -1,47 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/docker/compose-cli/aci/etchosts"
)
const hosts = "/etc/hosts"
func main() {
if len(os.Args) < 2 {
fmt.Fprint(os.Stderr, "usage: hosts HOSTNAME [HOSTNAME]")
os.Exit(1)
}
err := etchosts.SetHostNames(hosts, os.Args[1:]...)
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}
// ACI restart policy is currently at container group level, cannot let the sidecar terminate quietly once /etc/hosts has been edited
// Pause forever (until someone explicitly terminates this process ; go is not happy to stop all goroutines otherwise)
exitSignal := make(chan os.Signal, 1)
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM)
<-exitSignal
}

查看文件

@ -1,4 +0,0 @@
127.0.0.1 foo
127.0.0.1 bar
127.0.0.1 zot

查看文件

@ -1,153 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"encoding/json"
"strconv"
"time"
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/date"
"github.com/pkg/errors"
"github.com/docker/compose-cli/internal"
"github.com/docker/compose-cli/pkg/api"
)
// UserAgentName is the default user agent used by the cli
const UserAgentName = "docker-cli"
// NewContainerGroupsClient get client toi manipulate containerGrouos
func NewContainerGroupsClient(subscriptionID string) (containerinstance.ContainerGroupsClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return containerinstance.ContainerGroupsClient{}, err
}
containerGroupsClient := containerinstance.NewContainerGroupsClientWithBaseURI(mgmtURL, subscriptionID)
setupClient(&containerGroupsClient.Client, authorizer)
if err != nil {
return containerinstance.ContainerGroupsClient{}, err
}
containerGroupsClient.PollingDelay = 5 * time.Second
containerGroupsClient.RetryAttempts = 30
containerGroupsClient.RetryDuration = 1 * time.Second
return containerGroupsClient, nil
}
func setupClient(aciClient *autorest.Client, auth autorest.Authorizer) {
aciClient.UserAgent = UserAgentName + "/" + internal.Version
aciClient.Authorizer = auth
}
// NewStorageAccountsClient get client to manipulate storage accounts
func NewStorageAccountsClient(subscriptionID string) (storage.AccountsClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return storage.AccountsClient{}, err
}
storageAccuntsClient := storage.NewAccountsClientWithBaseURI(mgmtURL, subscriptionID)
setupClient(&storageAccuntsClient.Client, authorizer)
storageAccuntsClient.PollingDelay = 5 * time.Second
storageAccuntsClient.RetryAttempts = 30
storageAccuntsClient.RetryDuration = 1 * time.Second
return storageAccuntsClient, nil
}
// NewFileShareClient get client to manipulate file shares
func NewFileShareClient(subscriptionID string) (storage.FileSharesClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return storage.FileSharesClient{}, err
}
fileSharesClient := storage.NewFileSharesClientWithBaseURI(mgmtURL, subscriptionID)
setupClient(&fileSharesClient.Client, authorizer)
fileSharesClient.PollingDelay = 5 * time.Second
fileSharesClient.RetryAttempts = 30
fileSharesClient.RetryDuration = 1 * time.Second
return fileSharesClient, nil
}
// NewSubscriptionsClient get subscription client
func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return subscription.SubscriptionsClient{}, errors.Wrap(api.ErrLoginRequired, err.Error())
}
subc := subscription.NewSubscriptionsClientWithBaseURI(mgmtURL)
setupClient(&subc.Client, authorizer)
return subc, nil
}
// NewGroupsClient get client to manipulate groups
func NewGroupsClient(subscriptionID string) (resources.GroupsClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return resources.GroupsClient{}, err
}
groupsClient := resources.NewGroupsClientWithBaseURI(mgmtURL, subscriptionID)
setupClient(&groupsClient.Client, authorizer)
return groupsClient, nil
}
// NewContainerClient get client to manipulate containers
func NewContainerClient(subscriptionID string) (containerinstance.ContainersClient, error) {
authorizer, mgmtURL, err := getClientSetupData()
if err != nil {
return containerinstance.ContainersClient{}, err
}
containerClient := containerinstance.NewContainersClientWithBaseURI(mgmtURL, subscriptionID)
setupClient(&containerClient.Client, authorizer)
return containerClient, nil
}
func getClientSetupData() (autorest.Authorizer, string, error) {
return getClientSetupDataImpl(GetTokenStorePath())
}
func getClientSetupDataImpl(tokenStorePath string) (autorest.Authorizer, string, error) {
als, err := newAzureLoginServiceFromPath(tokenStorePath, azureAPIHelper{}, CloudEnvironments)
if err != nil {
return nil, "", err
}
oauthToken, _, err := als.GetValidToken()
if err != nil {
return nil, "", errors.Wrap(err, "not logged in to azure, you need to run \"docker login azure\" first")
}
ce, err := als.GetCloudEnvironment()
if err != nil {
return nil, "", err
}
token := adal.Token{
AccessToken: oauthToken.AccessToken,
Type: oauthToken.TokenType,
ExpiresIn: json.Number(strconv.Itoa(int(time.Until(oauthToken.Expiry).Seconds()))),
ExpiresOn: json.Number(strconv.Itoa(int(oauthToken.Expiry.Sub(date.UnixEpoch()).Seconds()))),
RefreshToken: "",
Resource: "",
}
return autorest.NewBearerAuthorizer(&token), ce.ResourceManagerURL, nil
}

查看文件

@ -1,36 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
func TestClearErrorMessageIfNotAlreadyLoggedIn(t *testing.T) {
dir, err := ioutil.TempDir("", "test_store")
assert.NilError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
_, _, err = getClientSetupDataImpl(filepath.Join(dir, tokenStoreFilename))
assert.ErrorContains(t, err, "not logged in to azure, you need to run \"docker login azure\" first")
}

查看文件

@ -1,274 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"github.com/pkg/errors"
)
const (
// AzurePublicCloudName is the moniker of the Azure public cloud
AzurePublicCloudName = "AzureCloud"
// AcrSuffixKey is the well-known name of the DNS suffix for Azure Container Registries
AcrSuffixKey = "acrLoginServer"
// CloudMetadataURLVar is the name of the environment variable that (if defined), points to a URL that should be used by Docker CLI to retrieve cloud metadata
CloudMetadataURLVar = "ARM_CLOUD_METADATA_URL"
// DefaultCloudMetadataURL is the URL of the cloud metadata service maintained by Azure public cloud
DefaultCloudMetadataURL = "https://management.azure.com/metadata/endpoints?api-version=2019-05-01"
)
// CloudEnvironmentService exposed metadata about Azure cloud environments
type CloudEnvironmentService interface {
Get(name string) (CloudEnvironment, error)
}
type cloudEnvironmentService struct {
cloudEnvironments map[string]CloudEnvironment
cloudMetadataURL string
// True if we have queried the cloud metadata endpoint already.
// We do it only once per CLI invocation.
metadataQueried bool
}
var (
// CloudEnvironments is the default instance of the CloudEnvironmentService
CloudEnvironments CloudEnvironmentService
)
func init() {
CloudEnvironments = newCloudEnvironmentService()
}
// CloudEnvironmentAuthentication data for logging into, and obtaining tokens for, Azure sovereign clouds
type CloudEnvironmentAuthentication struct {
LoginEndpoint string `json:"loginEndpoint"`
Audiences []string `json:"audiences"`
Tenant string `json:"tenant"`
}
// CloudEnvironment describes Azure sovereign cloud instance (e.g. Azure public, Azure US government, Azure China etc.)
type CloudEnvironment struct {
Name string `json:"name"`
Authentication CloudEnvironmentAuthentication `json:"authentication"`
ResourceManagerURL string `json:"resourceManager"`
Suffixes map[string]string `json:"suffixes"`
}
func newCloudEnvironmentService() *cloudEnvironmentService {
retval := cloudEnvironmentService{
metadataQueried: false,
}
retval.resetCloudMetadata()
return &retval
}
func (ces *cloudEnvironmentService) Get(name string) (CloudEnvironment, error) {
if ce, present := ces.cloudEnvironments[name]; present {
return ce, nil
}
if !ces.metadataQueried {
ces.metadataQueried = true
if ces.cloudMetadataURL == "" {
ces.cloudMetadataURL = os.Getenv(CloudMetadataURLVar)
if _, err := url.ParseRequestURI(ces.cloudMetadataURL); err != nil {
ces.cloudMetadataURL = DefaultCloudMetadataURL
}
}
res, err := http.Get(ces.cloudMetadataURL)
if err != nil {
return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
}
if res.StatusCode != 200 {
return CloudEnvironment{}, errors.Errorf("Cloud metadata retrieval from '%s' failed: server response was '%s'", ces.cloudMetadataURL, res.Status)
}
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
}
if err = ces.applyCloudMetadata(bytes); err != nil {
return CloudEnvironment{}, fmt.Errorf("Cloud metadata retrieval from '%s' failed: %w", ces.cloudMetadataURL, err)
}
}
if ce, present := ces.cloudEnvironments[name]; present {
return ce, nil
}
return CloudEnvironment{}, errors.Errorf("Cloud environment '%s' was not found", name)
}
func (ces *cloudEnvironmentService) applyCloudMetadata(jsonBytes []byte) error {
input := []CloudEnvironment{}
if err := json.Unmarshal(jsonBytes, &input); err != nil {
return err
}
newEnvironments := make(map[string]CloudEnvironment, len(input))
// If _any_ of the submitted data is invalid, we bail out.
for _, e := range input {
if len(e.Name) == 0 {
return errors.New("Azure cloud environment metadata is invalid (an environment with no name has been encountered)")
}
e.normalizeURLs()
if _, err := url.ParseRequestURI(e.Authentication.LoginEndpoint); err != nil {
return errors.Errorf("Metadata of cloud environment '%s' has invalid login endpoint URL: %s", e.Name, e.Authentication.LoginEndpoint)
}
if _, err := url.ParseRequestURI(e.ResourceManagerURL); err != nil {
return errors.Errorf("Metadata of cloud environment '%s' has invalid resource manager URL: %s", e.Name, e.ResourceManagerURL)
}
if len(e.Authentication.Audiences) == 0 {
return errors.Errorf("Metadata of cloud environment '%s' is invalid (no authentication audiences)", e.Name)
}
newEnvironments[e.Name] = e
}
for name, e := range newEnvironments {
ces.cloudEnvironments[name] = e
}
return nil
}
func (ces *cloudEnvironmentService) resetCloudMetadata() {
azurePublicCloud := CloudEnvironment{
Name: AzurePublicCloudName,
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.microsoftonline.com",
Audiences: []string{
"https://management.core.windows.net",
"https://management.azure.com",
},
Tenant: "common",
},
ResourceManagerURL: "https://management.azure.com",
Suffixes: map[string]string{
AcrSuffixKey: "azurecr.io",
},
}
azureChinaCloud := CloudEnvironment{
Name: "AzureChinaCloud",
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.chinacloudapi.cn",
Audiences: []string{
"https://management.core.chinacloudapi.cn",
"https://management.chinacloudapi.cn",
},
Tenant: "common",
},
ResourceManagerURL: "https://management.chinacloudapi.cn",
Suffixes: map[string]string{
AcrSuffixKey: "azurecr.cn",
},
}
azureUSGovernment := CloudEnvironment{
Name: "AzureUSGovernment",
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.microsoftonline.us",
Audiences: []string{
"https://management.core.usgovcloudapi.net",
"https://management.usgovcloudapi.net",
},
Tenant: "common",
},
ResourceManagerURL: "https://management.usgovcloudapi.net",
Suffixes: map[string]string{
AcrSuffixKey: "azurecr.us",
},
}
azureGermanCloud := CloudEnvironment{
Name: "AzureGermanCloud",
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.microsoftonline.de",
Audiences: []string{
"https://management.core.cloudapi.de",
"https://management.microsoftazure.de",
},
Tenant: "common",
},
ResourceManagerURL: "https://management.microsoftazure.de",
// There is no separate container registry suffix for German cloud
Suffixes: map[string]string{},
}
ces.cloudEnvironments = map[string]CloudEnvironment{
azurePublicCloud.Name: azurePublicCloud,
azureChinaCloud.Name: azureChinaCloud,
azureUSGovernment.Name: azureUSGovernment,
azureGermanCloud.Name: azureGermanCloud,
}
}
// GetTenantQueryURL returns an URL that can be used to fetch the list of Azure Active Directory tenants from a given cloud environment
func (ce *CloudEnvironment) GetTenantQueryURL() string {
tenantURL := ce.ResourceManagerURL + "/tenants?api-version=2019-11-01"
return tenantURL
}
// GetTokenScope returns a token scope that fits Docker CLI Azure management API usage
func (ce *CloudEnvironment) GetTokenScope() string {
scope := "offline_access " + ce.ResourceManagerURL + "/.default"
return scope
}
// GetAuthorizeRequestFormat returns a string format that can be used to construct authorization code request in an OAuth2 flow.
// The URL uses login endpoint appropriate for given cloud environment.
func (ce *CloudEnvironment) GetAuthorizeRequestFormat() string {
authorizeFormat := ce.Authentication.LoginEndpoint + "/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
return authorizeFormat
}
// GetTokenRequestFormat returns a string format that can be used to construct a security token request against Azure Active Directory
func (ce *CloudEnvironment) GetTokenRequestFormat() string {
tokenEndpoint := ce.Authentication.LoginEndpoint + "/%s/oauth2/v2.0/token"
return tokenEndpoint
}
func (ce *CloudEnvironment) normalizeURLs() {
ce.ResourceManagerURL = removeTrailingSlash(ce.ResourceManagerURL)
ce.Authentication.LoginEndpoint = removeTrailingSlash(ce.Authentication.LoginEndpoint)
for i, s := range ce.Authentication.Audiences {
ce.Authentication.Audiences[i] = removeTrailingSlash(s)
}
}
func removeTrailingSlash(s string) string {
return strings.TrimSuffix(s, "/")
}

查看文件

@ -1,187 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"testing"
"gotest.tools/v3/assert"
)
func TestNormalizeCloudEnvironmentURLs(t *testing.T) {
ce := CloudEnvironment{
Name: "SecretCloud",
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.here.com/",
Audiences: []string{
"https://audience1",
"https://audience2/",
},
Tenant: "common",
},
ResourceManagerURL: "invalid URL",
}
ce.normalizeURLs()
assert.Equal(t, ce.Authentication.LoginEndpoint, "https://login.here.com")
assert.Equal(t, ce.Authentication.Audiences[0], "https://audience1")
assert.Equal(t, ce.Authentication.Audiences[1], "https://audience2")
}
func TestApplyInvalidCloudMetadataJSON(t *testing.T) {
ce := newCloudEnvironmentService()
bb := []byte(`This isn't really valid JSON`)
err := ce.applyCloudMetadata(bb)
assert.Assert(t, err != nil, "Cloud metadata was invalid, so the application should have failed")
ensureWellKnownCloudMetadata(t, ce)
}
func TestApplyInvalidCloudMetatada(t *testing.T) {
ce := newCloudEnvironmentService()
// No name (moniker) for the cloud
bb := []byte(`
[{
"authentication": {
"loginEndpoint": "https://login.docker.com/",
"audiences": [
"https://management.docker.com/",
"https://management.cli.docker.com/"
],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "https://management.docker.com/"
}]`)
err := ce.applyCloudMetadata(bb)
assert.ErrorContains(t, err, "no name")
ensureWellKnownCloudMetadata(t, ce)
// Invalid resource manager URL
bb = []byte(`
[{
"authentication": {
"loginEndpoint": "https://login.docker.com/",
"audiences": [
"https://management.docker.com/",
"https://management.cli.docker.com/"
],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"name": "DockerAzureCloud",
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "123"
}]`)
err = ce.applyCloudMetadata(bb)
assert.ErrorContains(t, err, "invalid resource manager URL")
ensureWellKnownCloudMetadata(t, ce)
// Invalid login endpoint
bb = []byte(`
[{
"authentication": {
"loginEndpoint": "456",
"audiences": [
"https://management.docker.com/",
"https://management.cli.docker.com/"
],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"name": "DockerAzureCloud",
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "https://management.docker.com/"
}]`)
err = ce.applyCloudMetadata(bb)
assert.ErrorContains(t, err, "invalid login endpoint")
ensureWellKnownCloudMetadata(t, ce)
// No audiences
bb = []byte(`
[{
"authentication": {
"loginEndpoint": "https://login.docker.com/",
"audiences": [ ],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"name": "DockerAzureCloud",
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "https://management.docker.com/"
}]`)
err = ce.applyCloudMetadata(bb)
assert.ErrorContains(t, err, "no authentication audiences")
ensureWellKnownCloudMetadata(t, ce)
}
func TestApplyCloudMetadata(t *testing.T) {
ce := newCloudEnvironmentService()
bb := []byte(`
[{
"authentication": {
"loginEndpoint": "https://login.docker.com/",
"audiences": [
"https://management.docker.com/",
"https://management.cli.docker.com/"
],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"name": "DockerAzureCloud",
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "https://management.docker.com/"
}]`)
err := ce.applyCloudMetadata(bb)
assert.NilError(t, err)
env, err := ce.Get("DockerAzureCloud")
assert.NilError(t, err)
assert.Equal(t, env.Authentication.LoginEndpoint, "https://login.docker.com")
ensureWellKnownCloudMetadata(t, ce)
}
func TestDefaultCloudMetadataPresent(t *testing.T) {
ensureWellKnownCloudMetadata(t, CloudEnvironments)
}
func ensureWellKnownCloudMetadata(t *testing.T, ce CloudEnvironmentService) {
// Make sure well-known public cloud information is still available
_, err := ce.Get(AzurePublicCloudName)
assert.NilError(t, err)
_, err = ce.Get("AzureChinaCloud")
assert.NilError(t, err)
_, err = ce.Get("AzureUSGovernment")
assert.NilError(t, err)
}

查看文件

@ -1,134 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os/exec"
"runtime"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/pkg/errors"
)
var (
letterRunes = []rune("abcdefghijklmnopqrstuvwxyz123456789")
)
type apiHelper interface {
queryToken(ce CloudEnvironment, data url.Values, tenantID string) (azureToken, error)
openAzureLoginPage(redirectURL string, ce CloudEnvironment) error
queryAPIWithHeader(ctx context.Context, authorizationURL string, authorizationHeader string) ([]byte, int, error)
getDeviceCodeFlowToken(ce CloudEnvironment) (adal.Token, error)
}
type azureAPIHelper struct{}
func (helper azureAPIHelper) getDeviceCodeFlowToken(ce CloudEnvironment) (adal.Token, error) {
deviceconfig := auth.NewDeviceFlowConfig(clientID, "common")
deviceconfig.Resource = ce.ResourceManagerURL
spToken, err := deviceconfig.ServicePrincipalToken()
if err != nil {
return adal.Token{}, err
}
return spToken.Token(), err
}
func (helper azureAPIHelper) openAzureLoginPage(redirectURL string, ce CloudEnvironment) error {
state := randomString("", 10)
authURL := fmt.Sprintf(ce.GetAuthorizeRequestFormat(), clientID, redirectURL, state, ce.GetTokenScope())
return openbrowser(authURL)
}
func (helper azureAPIHelper) queryAPIWithHeader(ctx context.Context, authorizationURL string, authorizationHeader string) ([]byte, int, error) {
req, err := http.NewRequest(http.MethodGet, authorizationURL, nil)
if err != nil {
return nil, 0, err
}
req = req.WithContext(ctx)
req.Header.Add("Authorization", authorizationHeader)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, err
}
bits, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, 0, err
}
return bits, res.StatusCode, nil
}
func (helper azureAPIHelper) queryToken(ce CloudEnvironment, data url.Values, tenantID string) (azureToken, error) {
res, err := http.Post(fmt.Sprintf(ce.GetTokenRequestFormat(), tenantID), "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return azureToken{}, err
}
if res.StatusCode != 200 {
return azureToken{}, errors.Errorf("error while renewing access token, status : %s", res.Status)
}
bits, err := ioutil.ReadAll(res.Body)
if err != nil {
return azureToken{}, err
}
token := azureToken{}
if err := json.Unmarshal(bits, &token); err != nil {
return azureToken{}, err
}
return token, nil
}
func openbrowser(address string) error {
switch runtime.GOOS {
case "linux":
if isWsl() {
return exec.Command("wslview", address).Run()
}
return exec.Command("xdg-open", address).Run()
case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", address).Run()
case "darwin":
return exec.Command("open", address).Run()
default:
return fmt.Errorf("unsupported platform")
}
}
func isWsl() bool {
b, err := ioutil.ReadFile("/proc/version")
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(b)), "microsoft")
}
func randomString(prefix string, length int) string {
b := make([]rune, length)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return prefix + string(b)
}

查看文件

@ -1,135 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"fmt"
"net"
"net/http"
"net/url"
"github.com/pkg/errors"
)
const failHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Login failed</title>
</head>
<body>
<h4>Some failures occurred during the authentication</h4>
<p>You can log an issue at <a href="https://github.com/azure/azure-cli/issues">Azure CLI GitHub Repository</a> and we will assist you in resolving it.</p>
</body>
</html>
`
const successHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="10;url=https://docs.docker.com/engine/context/aci-integration/">
<title>Login successfully</title>
</head>
<body>
<h4>You have logged into Microsoft Azure!</h4>
<p>You can close this window, or we will redirect you to the <a href="https://docs.docker.com/engine/context/aci-integration/">Docker ACI integration documentation</a> in 10 seconds.</p>
</body>
</html>
`
const (
// redirectHost is where the user's browser is redirected on authentication
// completion. Note that only registered hosts can be used. i.e.:
// "localhost" works but "127.0.0.1" does not.
redirectHost = "localhost"
)
type localResponse struct {
values url.Values
err error
}
// LocalServer is an Azure login server
type LocalServer struct {
httpServer *http.Server
listener net.Listener
queryCh chan localResponse
}
// NewLocalServer creates an Azure login server
func NewLocalServer(queryCh chan localResponse) (*LocalServer, error) {
s := &LocalServer{queryCh: queryCh}
mux := http.NewServeMux()
mux.HandleFunc("/", s.handler())
s.httpServer = &http.Server{Handler: mux}
l, err := net.Listen("tcp", redirectHost+":0")
if err != nil {
return nil, err
}
s.listener = l
p := l.Addr().(*net.TCPAddr).Port
if p == 0 {
return nil, errors.New("unable to allocate login server port")
}
return s, nil
}
// Serve starts the local Azure login server
func (s *LocalServer) Serve() {
go func() {
if err := s.httpServer.Serve(s.listener); err != nil {
s.queryCh <- localResponse{
err: errors.Wrap(err, "unable to start login server"),
}
}
}()
}
// Close stops the local Azure login server
func (s *LocalServer) Close() {
_ = s.httpServer.Close()
}
// Addr returns the address that the local Azure server is service to
func (s *LocalServer) Addr() string {
return fmt.Sprintf("http://%s:%d", redirectHost, s.listener.Addr().(*net.TCPAddr).Port)
}
func (s *LocalServer) handler() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if _, hasCode := r.URL.Query()["code"]; hasCode {
if _, err := w.Write([]byte(successHTML)); err != nil {
s.queryCh <- localResponse{
err: errors.Wrap(err, "unable to write success HTML"),
}
} else {
s.queryCh <- localResponse{values: r.URL.Query()}
}
} else {
if _, err := w.Write([]byte(failHTML)); err != nil {
s.queryCh <- localResponse{
err: errors.Wrap(err, "unable to write fail HTML"),
}
} else {
s.queryCh <- localResponse{values: r.URL.Query()}
}
}
}
}

查看文件

@ -1,334 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
"github.com/docker/compose-cli/pkg/api"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
//go login process, derived from code sample provided by MS at https://github.com/devigned/go-az-cli-stuff
const (
clientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" // Azure CLI client id
)
type (
azureToken struct {
Type string `json:"token_type"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Foci string `json:"foci"`
}
tenantResult struct {
Value []tenantValue `json:"value"`
}
tenantValue struct {
TenantID string `json:"tenantId"`
}
)
// AzureLoginService Service to log into azure and get authentifier for azure APIs
type AzureLoginService interface {
Login(ctx context.Context, requestedTenantID string, cloudEnvironment string) error
LoginServicePrincipal(clientID string, clientSecret string, tenantID string, cloudEnvironment string) error
Logout(ctx context.Context) error
GetCloudEnvironment() (CloudEnvironment, error)
GetValidToken() (oauth2.Token, string, error)
}
type azureLoginService struct {
tokenStore tokenStore
apiHelper apiHelper
cloudEnvironmentSvc CloudEnvironmentService
}
const tokenStoreFilename = "dockerAccessToken.json"
// NewAzureLoginService creates a NewAzureLoginService
func NewAzureLoginService() (AzureLoginService, error) {
return newAzureLoginServiceFromPath(GetTokenStorePath(), azureAPIHelper{}, CloudEnvironments)
}
func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper, ces CloudEnvironmentService) (*azureLoginService, error) {
store, err := newTokenStore(tokenStorePath)
if err != nil {
return nil, err
}
return &azureLoginService{
tokenStore: store,
apiHelper: helper,
cloudEnvironmentSvc: ces,
}, nil
}
// LoginServicePrincipal login with clientId / clientSecret from a service principal.
// The resulting token does not include a refresh token
func (login *azureLoginService) LoginServicePrincipal(clientID string, clientSecret string, tenantID string, cloudEnvironment string) error {
// Tried with auth2.NewUsernamePasswordConfig() but could not make this work with username / password, setting this for CI with clientID / clientSecret
creds := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID)
spToken, err := creds.ServicePrincipalToken()
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not login with service principal: %s", err)
}
err = spToken.Refresh()
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not login with service principal: %s", err)
}
token, err := spToOAuthToken(spToken.Token())
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not read service principal token expiry: %s", err)
}
loginInfo := TokenInfo{TenantID: tenantID, Token: token, CloudEnvironment: cloudEnvironment}
if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not store login info: %s", err)
}
return nil
}
// Logout remove azure token data
func (login *azureLoginService) Logout(ctx context.Context) error {
err := login.tokenStore.removeData()
if os.IsNotExist(err) {
return errors.New("No Azure login data to be removed")
}
return err
}
func (login *azureLoginService) getTenantAndValidateLogin(
ctx context.Context,
accessToken string,
refreshToken string,
requestedTenantID string,
ce CloudEnvironment,
) error {
bits, statusCode, err := login.apiHelper.queryAPIWithHeader(ctx, ce.GetTenantQueryURL(), fmt.Sprintf("Bearer %s", accessToken))
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "check auth failed: %s", err)
}
if statusCode != http.StatusOK {
return errors.Wrapf(api.ErrLoginFailed, "unable to login status code %d: %s", statusCode, bits)
}
var t tenantResult
if err := json.Unmarshal(bits, &t); err != nil {
return errors.Wrapf(api.ErrLoginFailed, "unable to unmarshal tenant: %s", err)
}
tenantID, err := getTenantID(t.Value, requestedTenantID)
if err != nil {
return errors.Wrap(api.ErrLoginFailed, err.Error())
}
tToken, err := login.refreshToken(refreshToken, tenantID, ce)
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "unable to refresh token: %s", err)
}
loginInfo := TokenInfo{TenantID: tenantID, Token: tToken, CloudEnvironment: ce.Name}
if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not store login info: %s", err)
}
return nil
}
// Login performs an Azure login through a web browser
func (login *azureLoginService) Login(ctx context.Context, requestedTenantID string, cloudEnvironment string) error {
ce, err := login.cloudEnvironmentSvc.Get(cloudEnvironment)
if err != nil {
return err
}
queryCh := make(chan localResponse, 1)
s, err := NewLocalServer(queryCh)
if err != nil {
return err
}
s.Serve()
defer s.Close()
redirectURL := s.Addr()
if redirectURL == "" {
return errors.Wrap(api.ErrLoginFailed, "empty redirect URL")
}
deviceCodeFlowCh := make(chan deviceCodeFlowResponse, 1)
if err = login.apiHelper.openAzureLoginPage(redirectURL, ce); err != nil {
login.startDeviceCodeFlow(deviceCodeFlowCh, ce)
}
select {
case <-ctx.Done():
return ctx.Err()
case dcft := <-deviceCodeFlowCh:
if dcft.err != nil {
return errors.Wrapf(api.ErrLoginFailed, "could not get token using device code flow: %s", err)
}
token := dcft.token
return login.getTenantAndValidateLogin(ctx, token.AccessToken, token.RefreshToken, requestedTenantID, ce)
case q := <-queryCh:
if q.err != nil {
return errors.Wrapf(api.ErrLoginFailed, "unhandled local login server error: %s", err)
}
code, hasCode := q.values["code"]
if !hasCode {
return errors.Wrap(api.ErrLoginFailed, "no login code")
}
data := url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": code,
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
}
token, err := login.apiHelper.queryToken(ce, data, "organizations")
if err != nil {
return errors.Wrapf(api.ErrLoginFailed, "access token request failed: %s", err)
}
return login.getTenantAndValidateLogin(ctx, token.AccessToken, token.RefreshToken, requestedTenantID, ce)
}
}
type deviceCodeFlowResponse struct {
token adal.Token
err error
}
func (login *azureLoginService) startDeviceCodeFlow(deviceCodeFlowCh chan deviceCodeFlowResponse, ce CloudEnvironment) {
fmt.Println("Could not automatically open a browser, falling back to Azure device code flow authentication")
go func() {
token, err := login.apiHelper.getDeviceCodeFlowToken(ce)
if err != nil {
deviceCodeFlowCh <- deviceCodeFlowResponse{err: err}
}
deviceCodeFlowCh <- deviceCodeFlowResponse{token: token}
}()
}
func getTenantID(tenantValues []tenantValue, requestedTenantID string) (string, error) {
if requestedTenantID == "" {
if len(tenantValues) < 1 {
return "", errors.Errorf("could not find azure tenant")
}
return tenantValues[0].TenantID, nil
}
for _, tValue := range tenantValues {
if tValue.TenantID == requestedTenantID {
return tValue.TenantID, nil
}
}
return "", errors.Errorf("could not find requested azure tenant %s", requestedTenantID)
}
func toOAuthToken(token azureToken) oauth2.Token {
expireTime := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
oauthToken := oauth2.Token{
RefreshToken: token.RefreshToken,
AccessToken: token.AccessToken,
Expiry: expireTime,
TokenType: token.Type,
}
return oauthToken
}
func spToOAuthToken(token adal.Token) (oauth2.Token, error) {
expiresIn, err := token.ExpiresIn.Int64()
if err != nil {
return oauth2.Token{}, err
}
expireTime := time.Now().Add(time.Duration(expiresIn) * time.Second)
oauthToken := oauth2.Token{
RefreshToken: token.RefreshToken,
AccessToken: token.AccessToken,
Expiry: expireTime,
TokenType: token.Type,
}
return oauthToken, nil
}
// GetValidToken returns an access token and associated tenant ID.
// Will refresh the token as necessary.
func (login *azureLoginService) GetValidToken() (oauth2.Token, string, error) {
loginInfo, err := login.tokenStore.readToken()
if err != nil {
return oauth2.Token{}, "", err
}
token := loginInfo.Token
tenantID := loginInfo.TenantID
if token.Valid() {
return token, tenantID, nil
}
ce, err := login.cloudEnvironmentSvc.Get(loginInfo.CloudEnvironment)
if err != nil {
return oauth2.Token{}, "", errors.Wrap(err, "access token request failed--cloud environment could not be determined.")
}
token, err = login.refreshToken(token.RefreshToken, tenantID, ce)
if err != nil {
return oauth2.Token{}, "", errors.Wrap(err, "access token request failed. Maybe you need to login to Azure again.")
}
err = login.tokenStore.writeLoginInfo(TokenInfo{TenantID: tenantID, Token: token, CloudEnvironment: ce.Name})
if err != nil {
return oauth2.Token{}, "", err
}
return token, tenantID, nil
}
// GeCloudEnvironment returns the cloud environment associated with the current authentication token (if we have one)
func (login *azureLoginService) GetCloudEnvironment() (CloudEnvironment, error) {
tokenInfo, err := login.tokenStore.readToken()
if err != nil {
return CloudEnvironment{}, err
}
cloudEnvironment, err := login.cloudEnvironmentSvc.Get(tokenInfo.CloudEnvironment)
if err != nil {
return CloudEnvironment{}, err
}
return cloudEnvironment, nil
}
func (login *azureLoginService) refreshToken(currentRefreshToken string, tenantID string, ce CloudEnvironment) (oauth2.Token, error) {
data := url.Values{
"grant_type": []string{"refresh_token"},
"client_id": []string{clientID},
"scope": []string{ce.GetTokenScope()},
"refresh_token": []string{currentRefreshToken},
}
token, err := login.apiHelper.queryToken(ce, data, tenantID)
if err != nil {
return oauth2.Token{}, err
}
return toOAuthToken(token), nil
}

查看文件

@ -1,594 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"context"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"reflect"
"sync/atomic"
"testing"
"time"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
"golang.org/x/oauth2"
)
func testLoginService(t *testing.T, apiHelperMock *MockAzureHelper, cloudEnvironmentSvc CloudEnvironmentService) (*azureLoginService, error) {
dir, err := ioutil.TempDir("", "test_store")
if err != nil {
return nil, err
}
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
ces := CloudEnvironments
if cloudEnvironmentSvc != nil {
ces = cloudEnvironmentSvc
}
return newAzureLoginServiceFromPath(filepath.Join(dir, tokenStoreFilename), apiHelperMock, ces)
}
func TestRefreshInValidToken(t *testing.T) {
data := url.Values{
"grant_type": []string{"refresh_token"},
"client_id": []string{clientID},
"scope": []string{"offline_access https://management.docker.com/.default"},
"refresh_token": []string{"refreshToken"},
}
helperMock := &MockAzureHelper{}
helperMock.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), data, "123456").Return(azureToken{
RefreshToken: "newRefreshToken",
AccessToken: "newAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
cloudEnvironmentSvcMock := &MockCloudEnvironmentService{}
cloudEnvironmentSvcMock.On("Get", "AzureDockerCloud").Return(CloudEnvironment{
Name: "AzureDockerCloud",
Authentication: CloudEnvironmentAuthentication{
LoginEndpoint: "https://login.docker.com",
Audiences: []string{
"https://management.docker.com",
"https://management-ext.docker.com",
},
Tenant: "common",
},
ResourceManagerURL: "https://management.docker.com",
Suffixes: map[string]string{},
}, nil)
azureLogin, err := testLoginService(t, helperMock, cloudEnvironmentSvcMock)
assert.NilError(t, err)
err = azureLogin.tokenStore.writeLoginInfo(TokenInfo{
TenantID: "123456",
Token: oauth2.Token{
AccessToken: "accessToken",
RefreshToken: "refreshToken",
Expiry: time.Now().Add(-1 * time.Hour),
TokenType: "Bearer",
},
CloudEnvironment: "AzureDockerCloud",
})
assert.NilError(t, err)
token, tenantID, err := azureLogin.GetValidToken()
assert.NilError(t, err)
assert.Equal(t, tenantID, "123456")
assert.Equal(t, token.AccessToken, "newAccessToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(token.Expiry))
storedToken, err := azureLogin.tokenStore.readToken()
assert.NilError(t, err)
assert.Equal(t, storedToken.Token.AccessToken, "newAccessToken")
assert.Equal(t, storedToken.Token.RefreshToken, "newRefreshToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(storedToken.Token.Expiry))
assert.Equal(t, storedToken.CloudEnvironment, "AzureDockerCloud")
}
func TestDoesNotRefreshValidToken(t *testing.T) {
expiryDate := time.Now().Add(1 * time.Hour)
azureLogin, err := testLoginService(t, nil, nil)
assert.NilError(t, err)
err = azureLogin.tokenStore.writeLoginInfo(TokenInfo{
TenantID: "123456",
Token: oauth2.Token{
AccessToken: "accessToken",
RefreshToken: "refreshToken",
Expiry: expiryDate,
TokenType: "Bearer",
},
CloudEnvironment: AzurePublicCloudName,
})
assert.NilError(t, err)
token, tenantID, err := azureLogin.GetValidToken()
assert.NilError(t, err)
assert.Equal(t, token.AccessToken, "accessToken")
assert.Equal(t, tenantID, "123456")
}
func TestTokenStoreAssumesAzurePublicCloud(t *testing.T) {
expiryDate := time.Now().Add(1 * time.Hour)
azureLogin, err := testLoginService(t, nil, nil)
assert.NilError(t, err)
err = azureLogin.tokenStore.writeLoginInfo(TokenInfo{
TenantID: "123456",
Token: oauth2.Token{
AccessToken: "accessToken",
RefreshToken: "refreshToken",
Expiry: expiryDate,
TokenType: "Bearer",
},
// Simulates upgrade from older version of Docker CLI that did not have cloud environment concept
CloudEnvironment: "",
})
assert.NilError(t, err)
token, tenantID, err := azureLogin.GetValidToken()
assert.NilError(t, err)
assert.Equal(t, tenantID, "123456")
assert.Equal(t, token.AccessToken, "accessToken")
ce, err := azureLogin.GetCloudEnvironment()
assert.NilError(t, err)
assert.Equal(t, ce.Name, AzurePublicCloudName)
}
func TestInvalidLogin(t *testing.T) {
m := &MockAzureHelper{}
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL := args.Get(0).(string)
err := queryKeyValue(redirectURL, "error", "access denied: login failed")
assert.NilError(t, err)
}).Return(nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(context.TODO(), "", AzurePublicCloudName)
assert.Error(t, err, "no login code: login failed")
}
func TestValidLogin(t *testing.T) {
var redirectURL string
ctx := context.TODO()
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
data := refreshTokenData("firstRefreshToken", ce)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
RefreshToken: "newRefreshToken",
AccessToken: "newAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "", AzurePublicCloudName)
assert.NilError(t, err)
loginToken, err := azureLogin.tokenStore.readToken()
assert.NilError(t, err)
assert.Equal(t, loginToken.Token.AccessToken, "newAccessToken")
assert.Equal(t, loginToken.Token.RefreshToken, "newRefreshToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(loginToken.Token.Expiry))
assert.Equal(t, loginToken.TenantID, "12345a7c-c56d-43e8-9549-dd230ce8a038")
assert.Equal(t, loginToken.Token.Type(), "Bearer")
assert.Equal(t, loginToken.CloudEnvironment, "AzureCloud")
}
func TestValidLoginRequestedTenant(t *testing.T) {
var redirectURL string
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
authBody := `{"value":[{"id":"/tenants/00000000-c56d-43e8-9549-dd230ce8a038","tenantId":"00000000-c56d-43e8-9549-dd230ce8a038"},
{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
ctx := context.TODO()
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
data := refreshTokenData("firstRefreshToken", ce)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
RefreshToken: "newRefreshToken",
AccessToken: "newAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "12345a7c-c56d-43e8-9549-dd230ce8a038", AzurePublicCloudName)
assert.NilError(t, err)
loginToken, err := azureLogin.tokenStore.readToken()
assert.NilError(t, err)
assert.Equal(t, loginToken.Token.AccessToken, "newAccessToken")
assert.Equal(t, loginToken.Token.RefreshToken, "newRefreshToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(loginToken.Token.Expiry))
assert.Equal(t, loginToken.TenantID, "12345a7c-c56d-43e8-9549-dd230ce8a038")
assert.Equal(t, loginToken.Token.Type(), "Bearer")
assert.Equal(t, loginToken.CloudEnvironment, "AzureCloud")
}
func TestLoginNoTenant(t *testing.T) {
var redirectURL string
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
ctx := context.TODO()
authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "00000000-c56d-43e8-9549-dd230ce8a038", AzurePublicCloudName)
assert.Error(t, err, "could not find requested azure tenant 00000000-c56d-43e8-9549-dd230ce8a038: login failed")
}
func TestLoginRequestedTenantNotFound(t *testing.T) {
var redirectURL string
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
ctx := context.TODO()
authBody := `{"value":[]}`
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "", AzurePublicCloudName)
assert.Error(t, err, "could not find azure tenant: login failed")
}
func TestLoginAuthorizationFailed(t *testing.T) {
var redirectURL string
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{ce.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
authBody := `[access denied]`
ctx := context.TODO()
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 400, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "", AzurePublicCloudName)
assert.Error(t, err, "unable to login status code 400: [access denied]: login failed")
}
func TestValidThroughDeviceCodeFlow(t *testing.T) {
m := &MockAzureHelper{}
ce, err := CloudEnvironments.Get(AzurePublicCloudName)
assert.NilError(t, err)
m.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Return(errors.New("Could not open browser"))
m.On("getDeviceCodeFlowToken", mock.AnythingOfType("CloudEnvironment")).Return(adal.Token{AccessToken: "firstAccessToken", RefreshToken: "firstRefreshToken"}, nil)
authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
ctx := context.TODO()
m.On("queryAPIWithHeader", ctx, ce.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
data := refreshTokenData("firstRefreshToken", ce)
m.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
RefreshToken: "newRefreshToken",
AccessToken: "newAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
azureLogin, err := testLoginService(t, m, nil)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "", AzurePublicCloudName)
assert.NilError(t, err)
loginToken, err := azureLogin.tokenStore.readToken()
assert.NilError(t, err)
assert.Equal(t, loginToken.Token.AccessToken, "newAccessToken")
assert.Equal(t, loginToken.Token.RefreshToken, "newRefreshToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(loginToken.Token.Expiry))
assert.Equal(t, loginToken.TenantID, "12345a7c-c56d-43e8-9549-dd230ce8a038")
assert.Equal(t, loginToken.Token.Type(), "Bearer")
assert.Equal(t, loginToken.CloudEnvironment, "AzureCloud")
}
func TestNonstandardCloudEnvironment(t *testing.T) {
dockerCloudMetadata := []byte(`
[{
"authentication": {
"loginEndpoint": "https://login.docker.com/",
"audiences": [
"https://management.docker.com/",
"https://management.cli.docker.com/"
],
"tenant": "F5773994-FE88-482E-9E33-6E799D250416"
},
"name": "AzureDockerCloud",
"suffixes": {
"acrLoginServer": "azurecr.docker.io"
},
"resourceManager": "https://management.docker.com/"
}]`)
var metadataReqCount int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(dockerCloudMetadata)
assert.NilError(t, err)
atomic.AddInt32(&metadataReqCount, 1)
}))
defer srv.Close()
cloudMetadataURL, cloudMetadataURLSet := os.LookupEnv(CloudMetadataURLVar)
if cloudMetadataURLSet {
defer func() {
err := os.Setenv(CloudMetadataURLVar, cloudMetadataURL)
assert.NilError(t, err)
}()
}
err := os.Setenv(CloudMetadataURLVar, srv.URL)
assert.NilError(t, err)
ctx := context.TODO()
ces := newCloudEnvironmentService()
ces.cloudMetadataURL = srv.URL
dockerCloudEnv, err := ces.Get("AzureDockerCloud")
assert.NilError(t, err)
helperMock := &MockAzureHelper{}
var redirectURL string
helperMock.On("openAzureLoginPage", mock.AnythingOfType("string"), mock.AnythingOfType("CloudEnvironment")).Run(func(args mock.Arguments) {
redirectURL = args.Get(0).(string)
err := queryKeyValue(redirectURL, "code", "123456879")
assert.NilError(t, err)
}).Return(nil)
helperMock.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), mock.MatchedBy(func(data url.Values) bool {
//Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
return reflect.DeepEqual(data, url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"code": []string{"123456879"},
"scope": []string{dockerCloudEnv.GetTokenScope()},
"redirect_uri": []string{redirectURL},
})
}), "organizations").Return(azureToken{
RefreshToken: "firstRefreshToken",
AccessToken: "firstAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
authBody := `{"value":[{"id":"/tenants/F5773994-FE88-482E-9E33-6E799D250416","tenantId":"F5773994-FE88-482E-9E33-6E799D250416"}]}`
helperMock.On("queryAPIWithHeader", ctx, dockerCloudEnv.GetTenantQueryURL(), "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
data := refreshTokenData("firstRefreshToken", dockerCloudEnv)
helperMock.On("queryToken", mock.AnythingOfType("login.CloudEnvironment"), data, "F5773994-FE88-482E-9E33-6E799D250416").Return(azureToken{
RefreshToken: "newRefreshToken",
AccessToken: "newAccessToken",
ExpiresIn: 3600,
Foci: "1",
}, nil)
azureLogin, err := testLoginService(t, helperMock, ces)
assert.NilError(t, err)
err = azureLogin.Login(ctx, "", "AzureDockerCloud")
assert.NilError(t, err)
loginToken, err := azureLogin.tokenStore.readToken()
assert.NilError(t, err)
assert.Equal(t, loginToken.Token.AccessToken, "newAccessToken")
assert.Equal(t, loginToken.Token.RefreshToken, "newRefreshToken")
assert.Assert(t, time.Now().Add(3500*time.Second).Before(loginToken.Token.Expiry))
assert.Equal(t, loginToken.TenantID, "F5773994-FE88-482E-9E33-6E799D250416")
assert.Equal(t, loginToken.Token.Type(), "Bearer")
assert.Equal(t, loginToken.CloudEnvironment, "AzureDockerCloud")
assert.Equal(t, metadataReqCount, int32(1))
}
// Don't warn about refreshToken parameter taking the same value for all invocations
// nolint:unparam
func refreshTokenData(refreshToken string, ce CloudEnvironment) url.Values {
return url.Values{
"grant_type": []string{"refresh_token"},
"client_id": []string{clientID},
"scope": []string{ce.GetTokenScope()},
"refresh_token": []string{refreshToken},
}
}
func queryKeyValue(redirectURL string, key string, value string) error {
req, err := http.NewRequest("GET", redirectURL, nil)
if err != nil {
return err
}
q := req.URL.Query()
q.Add(key, value)
req.URL.RawQuery = q.Encode()
client := &http.Client{}
_, err = client.Do(req)
return err
}
type MockAzureHelper struct {
mock.Mock
}
func (s *MockAzureHelper) getDeviceCodeFlowToken(ce CloudEnvironment) (adal.Token, error) {
args := s.Called(ce)
return args.Get(0).(adal.Token), args.Error(1)
}
func (s *MockAzureHelper) queryToken(ce CloudEnvironment, data url.Values, tenantID string) (token azureToken, err error) {
args := s.Called(ce, data, tenantID)
return args.Get(0).(azureToken), args.Error(1)
}
func (s *MockAzureHelper) queryAPIWithHeader(ctx context.Context, authorizationURL string, authorizationHeader string) ([]byte, int, error) {
args := s.Called(ctx, authorizationURL, authorizationHeader)
return args.Get(0).([]byte), args.Int(1), args.Error(2)
}
func (s *MockAzureHelper) openAzureLoginPage(redirectURL string, ce CloudEnvironment) error {
args := s.Called(redirectURL, ce)
return args.Error(0)
}
type MockCloudEnvironmentService struct {
mock.Mock
}
func (s *MockCloudEnvironmentService) Get(name string) (CloudEnvironment, error) {
args := s.Called(name)
return args.Get(0).(CloudEnvironment), args.Error(1)
}

查看文件

@ -1,55 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/docker/compose-cli/api/context/store"
)
// StorageLogin helper for Azure Storage Login
type StorageLogin interface {
// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login
GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error)
}
// StorageLoginImpl implementation of StorageLogin
type StorageLoginImpl struct {
AciContext store.AciContext
}
// GetAzureStorageAccountKey retrieves the storage account ket from the current azure login
func (helper StorageLoginImpl) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
if err != nil {
return "", err
}
result, err := client.ListKeys(ctx, helper.AciContext.ResourceGroup, accountName, "")
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("could not access storage account acountKeys for %s, using the azure login", accountName))
}
if result.Keys != nil && len((*result.Keys)) < 1 {
return "", fmt.Errorf("no key could be obtained for storage account %s from your azure login", accountName)
}
key := (*result.Keys)[0]
return *key.Value, nil
}

查看文件

@ -1,94 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"github.com/Azure/go-autorest/autorest/azure/cli"
"golang.org/x/oauth2"
)
type tokenStore struct {
filePath string
}
// TokenInfo data stored in tokenStore
type TokenInfo struct {
Token oauth2.Token `json:"oauthToken"`
TenantID string `json:"tenantId"`
CloudEnvironment string `json:"cloudEnvironment"`
}
func newTokenStore(path string) (tokenStore, error) {
parentFolder := filepath.Dir(path)
dir, err := os.Stat(parentFolder)
if os.IsNotExist(err) {
err = os.MkdirAll(parentFolder, 0700)
if err != nil {
return tokenStore{}, err
}
dir, err = os.Stat(parentFolder)
}
if err != nil {
return tokenStore{}, err
}
if !dir.Mode().IsDir() {
return tokenStore{}, errors.New("cannot use path " + path + " ; " + parentFolder + " already exists and is not a directory")
}
return tokenStore{
filePath: path,
}, nil
}
// GetTokenStorePath the path for token store
func GetTokenStorePath() string {
cliPath, _ := cli.AccessTokensPath()
return filepath.Join(filepath.Dir(cliPath), tokenStoreFilename)
}
func (store tokenStore) writeLoginInfo(info TokenInfo) error {
bytes, err := json.MarshalIndent(info, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(store.filePath, bytes, 0644)
}
func (store tokenStore) readToken() (TokenInfo, error) {
bytes, err := ioutil.ReadFile(store.filePath)
if err != nil {
return TokenInfo{}, err
}
loginInfo := TokenInfo{}
if err := json.Unmarshal(bytes, &loginInfo); err != nil {
return TokenInfo{}, err
}
if loginInfo.CloudEnvironment == "" {
loginInfo.CloudEnvironment = AzurePublicCloudName
}
return loginInfo, nil
}
func (store tokenStore) removeData() error {
return os.Remove(store.filePath)
}

查看文件

@ -1,59 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 login
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
func TestCreateStoreFromExistingFolder(t *testing.T) {
existingDir, err := ioutil.TempDir("", "test_store")
assert.NilError(t, err)
storePath := filepath.Join(existingDir, tokenStoreFilename)
store, err := newTokenStore(storePath)
assert.NilError(t, err)
assert.Equal(t, store.filePath, storePath)
}
func TestCreateStoreFromNonExistingFolder(t *testing.T) {
existingDir, err := ioutil.TempDir("", "test_store")
assert.NilError(t, err)
storePath := filepath.Join(existingDir, "new", tokenStoreFilename)
store, err := newTokenStore(storePath)
assert.NilError(t, err)
assert.Equal(t, store.filePath, storePath)
newDir, err := os.Stat(filepath.Join(existingDir, "new"))
assert.NilError(t, err)
assert.Assert(t, newDir.Mode().IsDir())
}
func TestErrorIfParentFolderIsAFile(t *testing.T) {
existingDir, err := ioutil.TempFile("", "test_store")
assert.NilError(t, err)
storePath := filepath.Join(existingDir.Name(), tokenStoreFilename)
_, err = newTokenStore(storePath)
assert.Error(t, err, "cannot use path "+storePath+" ; "+existingDir.Name()+" already exists and is not a directory")
}

查看文件

@ -1,124 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
"github.com/pkg/errors"
"github.com/docker/compose-cli/aci/login"
)
// ResourceGroupHelper interface to manage resource groups and subscription IDs
type ResourceGroupHelper interface {
GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error)
ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error)
GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error)
CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error)
DeleteAsync(ctx context.Context, subscriptionID string, resourceGroupName string) error
}
type aciResourceGroupHelperImpl struct {
}
// NewACIResourceGroupHelper create a new ResourceGroupHelper
func NewACIResourceGroupHelper() ResourceGroupHelper {
return aciResourceGroupHelperImpl{}
}
// GetGroup get a resource group from its name
func (mgt aciResourceGroupHelperImpl) GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error) {
gc, err := login.NewGroupsClient(subscriptionID)
if err != nil {
return resources.Group{}, err
}
return gc.Get(ctx, groupName)
}
// ListGroups list resource groups
func (mgt aciResourceGroupHelperImpl) ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error) {
gc, err := login.NewGroupsClient(subscriptionID)
if err != nil {
return nil, err
}
groupResponse, err := gc.List(ctx, "", nil)
if err != nil {
return nil, err
}
groups := groupResponse.Values()
for groupResponse.NotDone() {
err = groupResponse.NextWithContext(ctx)
if err != nil {
return nil, err
}
newValues := groupResponse.Values()
groups = append(groups, newValues...)
}
return groups, nil
}
// CreateOrUpdate create or update a resource group
func (mgt aciResourceGroupHelperImpl) CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error) {
gc, err := login.NewGroupsClient(subscriptionID)
if err != nil {
return resources.Group{}, err
}
return gc.CreateOrUpdate(ctx, resourceGroupName, parameters)
}
// DeleteAsync deletes a resource group. Does not wait for full deletion to return (long operation)
func (mgt aciResourceGroupHelperImpl) DeleteAsync(ctx context.Context, subscriptionID string, resourceGroupName string) (err error) {
gc, err := login.NewGroupsClient(subscriptionID)
if err != nil {
return err
}
_, err = gc.Delete(ctx, resourceGroupName)
return err
}
// GetSubscriptionIDs Return available subscription IDs based on azure login
func (mgt aciResourceGroupHelperImpl) GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error) {
c, err := login.NewSubscriptionsClient()
if err != nil {
return nil, err
}
res, err := c.List(ctx)
if err != nil {
return nil, err
}
subs := res.Values()
if len(subs) == 0 {
return nil, errors.New("no subscriptions found")
}
for res.NotDone() {
err = res.NextWithContext(ctx)
if err != nil {
return nil, err
}
subs = append(subs, res.Values()...)
}
return subs, nil
}

查看文件

@ -1,66 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/docker/compose-cli/aci/convert"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/api/resources"
)
type aciResourceService struct {
aciContext store.AciContext
}
func (cs *aciResourceService) Prune(ctx context.Context, request resources.PruneRequest) (resources.PruneResult, error) {
res, err := getACIContainerGroups(ctx, cs.aciContext.SubscriptionID, cs.aciContext.ResourceGroup)
result := resources.PruneResult{}
if err != nil {
return result, err
}
multierr := &multierror.Error{}
deleted := []string{}
cpus := 0.
mem := 0.
for _, containerGroup := range res {
if !request.Force && convert.GetGroupStatus(containerGroup) == "Node "+convert.StatusRunning {
continue
}
for _, container := range *containerGroup.Containers {
hostConfig := convert.ToHostConfig(container, containerGroup)
cpus += hostConfig.CPUReservation
mem += convert.BytesToGB(float64(hostConfig.MemoryReservation))
}
if !request.DryRun {
_, err := deleteACIContainerGroup(ctx, cs.aciContext, *containerGroup.Name)
multierr = multierror.Append(multierr, err)
}
deleted = append(deleted, *containerGroup.Name)
}
result.DeletedIDs = deleted
result.Summary = fmt.Sprintf("Total CPUs reclaimed: %.2f, total memory reclaimed: %.2f GB", cpus, mem)
return result, multierr.ErrorOrNil()
}

查看文件

@ -1,276 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 aci
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/compose-cli/aci/login"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/pkg/api"
"github.com/docker/compose-cli/pkg/progress"
)
type aciVolumeService struct {
aciContext store.AciContext
}
func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) {
accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
return nil, err
}
result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup)
if err != nil {
return nil, err
}
accounts := result.Value
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return nil, err
}
fileShares := []volumes.Volume{}
for _, account := range *accounts {
fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "")
if err != nil {
return nil, err
}
for fileSharePage.NotDone() {
values := fileSharePage.Values()
for _, fileShare := range values {
fileShares = append(fileShares, toVolume(*account.Name, *fileShare.Name))
}
if err := fileSharePage.NextWithContext(ctx); err != nil {
return nil, err
}
}
}
return fileShares, nil
}
// VolumeCreateOptions options to create a new ACI volume
type VolumeCreateOptions struct {
Account string
}
func (cs *aciVolumeService) Create(ctx context.Context, name string, options interface{}) (volumes.Volume, error) {
opts, ok := options.(*VolumeCreateOptions)
if !ok || opts == nil {
return volumes.Volume{}, errors.New("could not read Azure VolumeCreateOptions struct from generic parameter")
}
w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(opts.Account, progress.Working, "Validating"))
accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
}
account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
if err == nil {
w.Event(progress.NewEvent(opts.Account, progress.Done, "Use existing"))
} else if !account.HasHTTPStatus(http.StatusNotFound) {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
} else {
result, err := accountClient.CheckNameAvailability(ctx, storage.AccountCheckNameAvailabilityParameters{
Name: to.StringPtr(opts.Account),
Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
})
if err != nil {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
}
if !*result.NameAvailable {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, errors.New("error: " + *result.Message)
}
parameters := defaultStorageAccountParams(cs.aciContext)
w.Event(progress.CreatingEvent(opts.Account))
future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
if err != nil {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
}
if err := future.WaitForCompletionRef(ctx, accountClient.Client); err != nil {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
}
account, err = future.Result(accountClient)
if err != nil {
w.Event(progress.ErrorEvent(opts.Account))
return volumes.Volume{}, err
}
w.Event(progress.CreatedEvent(opts.Account))
}
w.Event(progress.CreatingEvent(name))
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return volumes.Volume{}, err
}
fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, name, "")
if err == nil {
w.Event(progress.ErrorEvent(name))
return volumes.Volume{}, errors.Wrapf(api.ErrAlreadyExists, "Azure fileshare %q already exists", name)
}
if !fileShare.HasHTTPStatus(http.StatusNotFound) {
w.Event(progress.ErrorEvent(name))
return volumes.Volume{}, err
}
fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, name, storage.FileShare{})
if err != nil {
w.Event(progress.ErrorEvent(name))
return volumes.Volume{}, err
}
w.Event(progress.CreatedEvent(name))
return toVolume(*account.Name, *fileShare.Name), nil
}
func checkVolumeUsage(ctx context.Context, aciContext store.AciContext, id string) error {
containerGroups, err := getACIContainerGroups(ctx, aciContext.SubscriptionID, aciContext.ResourceGroup)
if err != nil {
return err
}
for _, cg := range containerGroups {
if hasVolume(cg.Volumes, id) {
return errors.Errorf("volume %q is used in container group %q",
id, *cg.Name)
}
}
return nil
}
func hasVolume(volumes *[]containerinstance.Volume, id string) bool {
if volumes == nil {
return false
}
for _, v := range *volumes {
if v.AzureFile != nil && v.AzureFile.StorageAccountName != nil && v.AzureFile.ShareName != nil &&
(*v.AzureFile.StorageAccountName+"/"+*v.AzureFile.ShareName) == id {
return true
}
}
return false
}
func (cs *aciVolumeService) Delete(ctx context.Context, id string, options interface{}) error {
err := checkVolumeUsage(ctx, cs.aciContext, id)
if err != nil {
return err
}
storageAccount, fileshare, err := getStorageAccountAndFileshare(id)
if err != nil {
return err
}
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return err
}
fileShareItemsPage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, storageAccount, "", "", "")
if err != nil {
return err
}
fileshares := fileShareItemsPage.Values()
if len(fileshares) == 1 && *fileshares[0].Name == fileshare {
storageAccountsClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
if err != nil {
return err
}
account, err := storageAccountsClient.GetProperties(ctx, cs.aciContext.ResourceGroup, storageAccount, "")
if err != nil {
return err
}
if err == nil {
if _, ok := account.Tags[dockerVolumeTag]; ok {
result, err := storageAccountsClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount)
if result.IsHTTPStatus(http.StatusNoContent) {
return errors.Wrapf(api.ErrNotFound, "storage account %s does not exist", storageAccount)
}
return err
}
}
}
result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
if result.HasHTTPStatus(http.StatusNoContent) {
return errors.Wrapf(api.ErrNotFound, "fileshare %q", fileshare)
}
return err
}
func (cs *aciVolumeService) Inspect(ctx context.Context, id string) (volumes.Volume, error) {
storageAccount, fileshareName, err := getStorageAccountAndFileshare(id)
if err != nil {
return volumes.Volume{}, err
}
fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
if err != nil {
return volumes.Volume{}, err
}
res, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
if err != nil { // Just checks if it exists
if res.HasHTTPStatus(http.StatusNotFound) {
return volumes.Volume{}, errors.Wrapf(api.ErrNotFound, "account %q, file share %q. Original message %s", storageAccount, fileshareName, err.Error())
}
return volumes.Volume{}, err
}
return toVolume(storageAccount, fileshareName), nil
}
func toVolume(storageAccountName string, fileShareName string) volumes.Volume {
return volumes.Volume{
ID: volumeID(storageAccountName, fileShareName),
Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, storageAccountName),
}
}
func volumeID(storageAccount string, fileShareName string) string {
return fmt.Sprintf("%s/%s", storageAccount, fileShareName)
}
func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
tags := map[string]*string{dockerVolumeTag: to.StringPtr(dockerVolumeTag)}
return storage.AccountCreateParameters{
Location: to.StringPtr(aciContext.Location),
Sku: &storage.Sku{
Name: storage.StandardLRS,
},
Tags: tags,
}
}
func getStorageAccountAndFileshare(volumeID string) (string, string, error) {
tokens := strings.Split(volumeID, "/")
if len(tokens) != 2 {
return "", "", errors.New("invalid format for volume ID, expected storageaccount/fileshare")
}
return tokens[0], tokens[1], nil
}

查看文件

@ -1,118 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 backend
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"github.com/docker/compose-cli/api/cloud"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/resources"
"github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/pkg/api"
)
var (
errNoType = errors.New("backend: no type")
errNoName = errors.New("backend: no name")
errTypeRegistered = errors.New("backend: already registered")
)
type initFunc func() (Service, error)
type getCloudServiceFunc func() (cloud.Service, error)
type registeredBackend struct {
name string
backendType string
init initFunc
getCloudService getCloudServiceFunc
}
var backends = struct {
r []*registeredBackend
}{}
var instance Service
// Current return the active backend instance
func Current() Service {
return instance
}
// WithBackend set the active backend instance
func WithBackend(s Service) {
instance = s
}
// Service aggregates the service interfaces
type Service interface {
ContainerService() containers.Service
ComposeService() api.Service
ResourceService() resources.Service
SecretsService() secrets.Service
VolumeService() volumes.Service
}
// Register adds a typed backend to the registry
func Register(name string, backendType string, init initFunc, getCoudService getCloudServiceFunc) {
if name == "" {
logrus.Fatal(errNoName)
}
if backendType == "" {
logrus.Fatal(errNoType)
}
for _, b := range backends.r {
if b.backendType == backendType {
logrus.Fatal(errTypeRegistered)
}
}
backends.r = append(backends.r, &registeredBackend{
name,
backendType,
init,
getCoudService,
})
}
// Get returns the backend registered for a particular type, it returns
// an error if there is no registered backends for the given type.
func Get(backendType string) (Service, error) {
for _, b := range backends.r {
if b.backendType == backendType {
return b.init()
}
}
return nil, api.ErrNotFound
}
// GetCloudService returns the backend registered for a particular type, it returns
// an error if there is no registered backends for the given type.
func GetCloudService(backendType string) (cloud.Service, error) {
for _, b := range backends.r {
if b.backendType == backendType {
return b.getCloudService()
}
}
return nil, fmt.Errorf("backend not found for backend type %s", backendType)
}

查看文件

@ -1,119 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 client
import (
"context"
"github.com/docker/compose-cli/api/backend"
"github.com/docker/compose-cli/api/cloud"
"github.com/docker/compose-cli/api/containers"
apicontext "github.com/docker/compose-cli/api/context"
"github.com/docker/compose-cli/api/context/store"
"github.com/docker/compose-cli/api/resources"
"github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/pkg/api"
)
// New returns a backend client associated with current context
func New(ctx context.Context) (*Client, error) {
currentContext := apicontext.Current()
s := store.Instance()
cc, err := s.Get(currentContext)
if err != nil {
return nil, err
}
service := backend.Current()
if service == nil {
return nil, api.ErrNotFound
}
client := NewClient(cc.Type(), service)
return &client, nil
}
// NewClient returns new client
func NewClient(backendType string, service backend.Service) Client {
return Client{
backendType: backendType,
bs: service,
}
}
// GetCloudService returns a backend CloudService (typically login, create context)
func GetCloudService(ctx context.Context, backendType string) (cloud.Service, error) {
return backend.GetCloudService(backendType)
}
// Client is a multi-backend client
type Client struct {
backendType string
bs backend.Service
}
// ContextType the context type associated with backend
func (c *Client) ContextType() string {
return c.backendType
}
// ContainerService returns the backend service for the current context
func (c *Client) ContainerService() containers.Service {
if cs := c.bs.ContainerService(); cs != nil {
return cs
}
return &containerService{}
}
// ComposeService returns the backend service for the current context
func (c *Client) ComposeService() api.Service {
if cs := c.bs.ComposeService(); cs != nil {
return cs
}
return &composeService{}
}
// SecretsService returns the backend service for the current context
func (c *Client) SecretsService() secrets.Service {
if ss := c.bs.SecretsService(); ss != nil {
return ss
}
return &secretsService{}
}
// VolumeService returns the backend service for the current context
func (c *Client) VolumeService() volumes.Service {
if vs := c.bs.VolumeService(); vs != nil {
return vs
}
return &volumeService{}
}
// ResourceService returns the backend service for the current context
func (c *Client) ResourceService() resources.Service {
if vs := c.bs.ResourceService(); vs != nil {
return vs
}
return &resourceService{}
}

查看文件

@ -1,123 +0,0 @@
/*
Copyright 2020 Docker Compose CLI 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 client
import (
"context"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose-cli/pkg/api"
)
type composeService struct {
}
func (c *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Create(ctx context.Context, project *types.Project, opts api.CreateOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Start(ctx context.Context, project *types.Project, options api.StartOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Up(context.Context, *types.Project, api.UpOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Down(context.Context, string, api.DownOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Logs(context.Context, string, api.LogConsumer, api.LogOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Ps(context.Context, string, api.PsOptions) ([]api.ContainerSummary, error) {
return nil, api.ErrNotImplemented
}
func (c *composeService) List(context.Context, api.ListOptions) ([]api.Stack, error) {
return nil, api.ErrNotImplemented
}
func (c *composeService) Convert(context.Context, *types.Project, api.ConvertOptions) ([]byte, error) {
return nil, api.ErrNotImplemented
}
func (c *composeService) Kill(ctx context.Context, project *types.Project, options api.KillOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
return 0, api.ErrNotImplemented
}
func (c *composeService) Remove(ctx context.Context, project *types.Project, options api.RemoveOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Exec(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
return 0, api.ErrNotImplemented
}
func (c *composeService) Copy(ctx context.Context, project *types.Project, opts api.CopyOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Pause(ctx context.Context, project string, options api.PauseOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) UnPause(ctx context.Context, project string, options api.PauseOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Top(ctx context.Context, projectName string, services []string) ([]api.ContainerProcSummary, error) {
return nil, api.ErrNotImplemented
}
func (c *composeService) Events(ctx context.Context, project string, options api.EventsOptions) error {
return api.ErrNotImplemented
}
func (c *composeService) Port(ctx context.Context, project string, service string, port int, options api.PortOptions) (string, int, error) {
return "", 0, api.ErrNotImplemented
}
func (c *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
return nil, api.ErrNotImplemented
}

某些文件未显示,因为此 diff 中更改的文件太多 显示更多